<!DOCTYPE html>
<html lang="cn" style="">
<head>
	<title>PhalApi-Wikis | PhalApi(π框架) - PHP轻量级开源接口框架</title>
     <link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" media="screen">
    <link rel="stylesheet" type="text/css" href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.css" media="screen">
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<link rel="icon" href="http://webtools.qiniudn.com/dog_catch.png" type="image/x-icon" />
	<meta name="description" content="PhalApi是一个PHP轻量级开源接口框架。我们致力于将PhalApi维护成像恒星一样：不断更新，保持生气；为接口负责，为开源负责！让后台接口开发更简单！">
	<meta name="keywords" content="PhalApi,phalapi,phalapi接口开发,后台接口开发框架,后台接口开发,接口开发,接口框架,开源接口框架,PHP后台接口开发,PHP接口开发,PHP接口框架,PHP后台接口框架,phalapi官网,PHP接口框架,php接口开发框架,php接口开发,php web框架">
	<meta name="author" content="dogstar">

	<link rel="stylesheet" type="text/css" href="./../css/screen.css?20150211" />

    <!-- 代码高亮 -->
    <link rel="stylesheet" href="https://www.phalapi.net/css/highlight.min.css">
    <script src="https://www.phalapi.net/js/highlight.min.js"></script>
    <script>hljs.initHighlightingOnLoad();</script>

    <script src="https://www.phalapi.net/js/jquery-1.11.1.min.js"></script>
    <script src="https://www.phalapi.net/js/bootstrap.min.js"></script>
    <style type="text/css">
    @page {
        @media print {

h1 {

color: #000;

background: none;

}

nav, aside {

display: none;

}

body, article {

width: 100%;

margin: 0;

padding: 0;

}

@page {

margin: 2cm;

}

}
    /*页面设置：a4 横幅*/
    size: A4 portrait;
    margin: 9%;
}

@media print {   
    table {
        page-break-inside: avoid;
    }
}
        

    </style>
</head>

<body>
<a href="https://github.com/phalapi/phalapi/tree/release">
	<img alt="Fork me on GitHub" data-canonical-src="http://7xslqv.com1.z0.glb.clouddn.com/images/2/2d/9e69c0ebaff22a24e241a0244532e.png" src="http://7xslqv.com1.z0.glb.clouddn.com/images/2/2d/9e69c0ebaff22a24e241a0244532e.png" style="position: absolute; top: 0; right: 0; border: 0;">
</a>

<!-- 最顶部的语言(S) -->
<div class="grid-wrapper navbar desktop-only">
	<div class="grid">
		<div class="grid__cell">
			<ul id="language-switchers" class="navbar__links navbar--left">
				<li class="menu-item"><a title="PhalApi官方网站中文版" href="./" class="menu-item__link">中文版</a></li>
				<li class="menu-item"><a title="English version of PhalApi website" href="/wikis/en/" class="menu-item__link">English</a></li>
			</ul>
			<ul id="util-menu" class="navbar__links navbar--right navbar--vertical-separator">
				<li class="menu-item"><a href="#" class="menu-item__link contact-us-spec">官方交流群：459352221</a></li>
			</ul>
		</div>
	</div>
</div>
<!-- 最顶部的语言(E) -->

<!-- 顶部导航菜单(S) -->
<div class="grid-wrapper desktop-only">
	<div class="grid">
		<div class="grid__cell">
			<div class="header__header-wrapper">
				<a title="PhalApi" href="http://www.phalapi.net" class="header__logo"><img src="http://webtools.qiniudn.com/master-LOGO-20150410_33.jpg" id="tw-logo" alt="PhalApi"></a>
				<ul id="main-menu" class="header__menu">
					<li class="menu-item"><a href="http://qa.phalapi.net/" class="menu-item__link insights-spec" target="_blank">社区</a></li>
					<li class="menu-item"><a href="/download.html" class="menu-item__link insights-spec" target="_blank">下载</a></li>
					<li class="menu-item"><a href="/wikis/" class="menu-item__link events-spec" target="_blank">文档</a></li>
					<li class="menu-item"><a href="/wikis/%5b8.1%5d-PhalApi%e8%a7%86%e9%a2%91%e6%95%99%e7%a8%8b.html" class="menu-item__link events-spec" target="_blank">视频教程</a></li>
    				<li class="menu-item"><a href="/docs/" class="menu-item__link events-spec" target="_blank">类参考手册</a></li>
					<li class="menu-item"><a href="http://phalapi.oschina.mopaas.com/Public/demo/" class="menu-item__link products-spec" target="_blank">在线体验</a></li>
					<li class="menu-item"><a href="https://zb.oschina.net/service/736cc7c075dd6d0e" class="menu-item__link products-spec" target="_blank">增值服务</a></li>
					<li class="menu-item"><a href="/donate.html" class="menu-item__link about-us-spec" target="_blank">捐赠</a></li>
					<li class="menu-item"><a href="/about.html" class="menu-item__link about-us-spec" target="_blank">关于</a></li>
				</ul>
			</div>
		</div>
	</div>
</div>
<!-- 顶部导航菜单(E) -->


    <div class="grid-wrapper">
        <div class="grid">
            <div class="grid__cell">
                <h1>index</h1>
<h2>前言</h2>
<ul>
<li><a href=".">首页</a></li>
<li><a href="/wikis/%E6%8E%A5%E5%8F%A3%EF%BC%8C%E4%BB%8E%E7%AE%80%E5%8D%95%E5%BC%80%E5%A7%8B%EF%BC%81.html">接口，从简单开始！</a></li>
</ul>
<h2>一、基础入门</h2>
<h3>开篇</h3>
<ul>
<li><a href="/wikis/%5B1.1%5D-%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85.html">[1.1] 下载与安装</a> </li>
<li><a href="/wikis/%5B1.2%5D-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE.html">[1.2] 创建一个自己的项目(含四个脚本命令的使用说明)</a>  </li>
<li><a href="/wikis/%5B1.3%5D-%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C.html">[1.3] 在线体验</a></li>
<li><a href="/wikis/%5B1.4%5D-%E6%96%87%E6%A1%A3%E3%80%81%E5%B8%AE%E5%8A%A9%E5%92%8C%E5%AE%98%E7%BD%91.html">[1.4] 文档、帮助和官网</a></li>
<li><a href="/wikis/%5B1.10%5D-%E5%AF%B9PhalApi%E6%A1%86%E6%9E%B6%E7%9A%84%E6%8A%89%E6%8B%A9.html">[1.5] 对PhalApi框架的抉择</a></li>
</ul>
<h3>开发手册</h3>
<ul>
<li><a href="/wikis/%5B1.11%5D-%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%28backup%29.html">[1.11] 快速入门: 接口开发示例(源码+图文)</a></li>
<li><a href="/wikis/%5B1.12%5D-%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE.html">[1.12] 参数规则：接口参数规则配置</a></li>
<li><a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">[1.13] 统一的接口请求方式：?sevice=XXX.XXX</a></li>
<li><a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">[1.14] 统一的返回格式和结构：ret data msg</a></li>
<li><a href="/wikis/%5B1.15%5D-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%9F%BA%E4%BA%8ENotORM%E7%9A%84%E4%BD%BF%E7%94%A8%E5%8F%8A%E4%BC%98%E5%8C%96.html">[1.15] 数据库操作：基于NotORM的使用及优化</a></li>
<li><a href="/wikis/%5B1.16%5D-%E9%85%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%EF%BC%9A%E5%86%85%E5%A4%96%E7%BD%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E7%9A%84%E5%AE%8C%E7%BE%8E%E5%88%87%E6%8D%A2.html">[1.16] 配置读取：内外网环境配置的完美切换</a></li>
<li><a href="/wikis/%5B1.17%5D-%E6%97%A5%E8%AE%B0%E7%BA%AA%E5%BD%95%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84%E6%97%A5%E8%AE%B0%E6%8E%A5%E5%8F%A3.html">[1.17] 日记纪录：简化版的日记接口</a></li>
<li><a href="/wikis/%5B1.18%5D-%E5%BF%AB%E9%80%9F%E5%87%BD%E6%95%B0%EF%BC%9A%E4%BA%BA%E6%80%A7%E5%8C%96%E7%9A%84%E5%85%B3%E6%80%80.html">[1.18] 快速函数：人性化的关怀</a></li>
<li><a href="/wikis/%5B1.19%5D-DI%E6%9C%8D%E5%8A%A1%E9%80%9F%E6%9F%A5%EF%BC%9A%E5%90%84%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E4%B8%80%E8%A7%88%E8%A1%A8.html">[1.19] DI服务速查：各资源服务一览表 </a></li>
<li><a href="/wikis/%5b1.20%5d-DB%e6%93%8d%e4%bd%9c%ef%bc%9a%e6%95%b0%e6%8d%ae%e5%ba%93%e5%9f%ba%e6%9c%ac%e6%93%8d%e4%bd%9c%e9%80%9f%e6%9f%a5.html">[1.20] DB操作：数据库基本操作速查</a>  </li>
</ul>
<h3>更多技艺</h3>
<ul>
<li><a href="/wikis/%5B1.21%5D-%E7%B1%BB%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD%EF%BC%9A%E9%81%B5%E5%BE%AAPEAR%E5%8C%85%E7%9A%84%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.html">[1.21] 类的自动加载：遵循PEAR包的命名规范</a></li>
<li><a href="/wikis/%5B1.22%5D-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AD%BE%E5%90%8D%E8%A7%84%E5%88%99.html">[1.22] 签名验证：自定义签名规则</a></li>
<li><a href="/wikis/%5B1.23%5D-%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%EF%BC%9AGET%E5%92%8CPOST%E4%B8%A4%E8%80%85%E7%9A%86%E5%8F%AF%E5%BE%97%E5%8F%8A%E8%B6%85%E8%B6%8AJSON%E6%A0%BC%E5%BC%8F%E8%BF%94%E5%9B%9E.html">[1.23] 请求和响应：GET和POST两者皆可得及超越JSON格式返回</a></li>
<li><a href="/wikis/%5B1.24%5D-%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E5%9C%B0%E5%8F%AF%E9%85%8D%E7%BD%AE%E5%8C%96%E7%9A%84%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.html">[1.24] 缓存策略：更灵活地可配置化的多级缓存</a></li>
<li><a href="/wikis/%5B1.25%5D-%E5%9B%BD%E9%99%85%E5%8C%96%E7%BF%BB%E8%AF%91%EF%BC%9A%E4%B8%BA%E8%B5%B0%E5%90%91%E5%9B%BD%E9%99%85%E5%8C%96%E6%8F%90%E5%89%8D%E5%81%9A%E5%A5%BD%E7%BF%BB%E8%AF%91%E5%87%86%E5%A4%87.html">[1.25] 国际化翻译：为走向国际化提前做好翻译准备</a></li>
<li><a href="/wikis/%5B1.26%5D-%E6%95%B0%E6%8D%AE%E5%AE%89%E5%85%A8%EF%BC%9A%E6%95%B0%E6%8D%AE%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E6%96%B9%E6%A1%88.html">[1.26] 数据安全：数据对称加密方案</a></li>
<li><a href="/wikis/%5B1.27%5D-%E7%B2%BE%E7%9B%8A%E5%BC%80%E5%8F%91%EF%BC%9A%E6%9B%B4%E5%AF%8C%E8%A1%A8%E7%8E%B0%E5%8A%9B%E7%9A%84Model%E5%B1%82%E5%92%8C%E9%87%8D%E9%87%8F%E7%BA%A7%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E7%9A%84%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88.html">[1.27] 精益开发：更富表现力的Model层和重量级数据获取的应对方案</a> </li>
<li><a href="/wikis/%5B1.28%5D-COOKIE%EF%BC%9A%E5%AF%B9COOKIE%E5%8E%9F%E7%94%9F%E6%80%81%E7%9A%84%E6%94%AF%E6%8C%81%E5%8F%8A%E8%AE%B0%E5%BF%86%E5%8A%A0%E5%AF%86%E5%8D%87%E7%BA%A7%E7%89%88.html">[1.28] COOKIE：对COOKIE原生态的支持及记忆加密升级版</a> </li>
<li><a href="/wikis/%5B1.29%5D-%E5%BC%80%E6%94%BE%E4%B8%8E%E5%B0%81%E9%97%AD%EF%BC%9A%E5%A4%9A%E5%85%A5%E5%8F%A3%E5%92%8C%E7%BB%9F%E4%B8%80%E5%88%9D%E5%A7%8B%E5%8C%96.html">[1.29] 开放与封闭：多入口和统一初始化</a>  </li>
<li><a href="/wikis/%5B1.30%5D-%E4%BF%9D%E6%8C%81%E7%9A%84%E5%8A%9B%E9%87%8F%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html">[1.30] 保持的力量：接口开发最佳实践</a>  </li>
<li><a href="/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">[1.31] 新型计划任务：以接口形式实现的计划任务</a>  </li>
<li><a href="/wikis/%5B1.32%5D-%E5%9C%A8%E7%BA%BF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%EF%BC%9A%E6%B3%A8%E9%87%8A%E8%A7%84%E8%8C%83.html">[1.32] 在线接口文档：注释规范</a></li>
</ul>
<h2>二、高级专题</h2>
<h3>思想与应用</h3>
<ul>
<li><a href="/wikis/%5B2.11%5D-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%EF%BC%9ADI%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5-%E8%AE%A9%E8%B5%84%E6%BA%90%E6%9B%B4%E5%8F%AF%E6%8E%A7.html">[2.11] 核心思想：Di依赖注入 让资源更可控</a></li>
<li><a href="/wikis/%5B2.12%5D-%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%EF%BC%9A%E5%8F%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html">[2.12] 海量数据：可配置的分库分表</a></li>
<li><a href="/wikis/%5B2.13%5D-%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95%EF%BC%9A%E5%9C%A8%E7%BA%BFSQL%E8%AF%AD%E5%8F%A5%E6%9F%A5%E7%9C%8B%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html">[2.13] 接口调试：在线sql语句查看与性能优化</a></li>
<li><a href="/wikis/%5B2.14%5D-%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%EF%BC%9A%E6%84%8F%E5%9B%BE%E5%AF%BC%E5%90%91%E7%BC%96%E7%A8%8B%E4%B8%8B%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91.html">[2.14] 测试驱动开发：意图导向编程下的接口开发</a></li>
<li><a href="/wikis/%5B2.15%5D-%E6%BC%94%E8%BF%9B%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%E7%BB%AD%E7%AF%87.html">[2.15] 演进：新型计划任务续篇</a></li>
<li><a href="/wikis/%5B2.16%5D-%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%BA%94%E5%AF%B9%E5%A4%8D%E6%9D%82%E9%A2%86%E5%9F%9F%E4%B8%9A%E5%8A%A1%E7%9A%84Domain%E5%B1%82-.html">[2.16] 领域驱动设计：应对复杂领域业务的Domain层</a></li>
<li><a href="/wikis/%5B2.17%5D-%E5%BE%AE%E6%9C%8D%E5%8A%A1%EF%BC%9AApi%E6%8E%A5%E5%8F%A3%E6%9C%8D%E5%8A%A1%E5%B1%82.html">[2.17] 微服务：Api接口服务层</a></li>
<li><a href="/wikis/%5B2.18%5D-%E5%AE%9A%E5%88%B6%E5%8C%96%EF%BC%9A%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E7%9A%84%E5%86%8D%E5%AE%9E%E7%8E%B0.html">[2.18] 定制化：资源服务的再实现</a></li>
<li><a href="/wikis/%5B2.19%5D-%E6%89%A9%E5%B1%95%E5%BA%93%EF%BC%9A%E5%8F%AF%E9%87%8D%E7%94%A8%E7%9A%84%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93.html">[2.19] 扩展库：可重用的扩展类库 </a></li>
<li><a href="/wikis/%5B2.20%5D-%E7%BA%A6%E5%AE%9A%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%9E%B6%E6%9E%84%E6%98%8E%E6%98%BE%E7%9A%84%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC.html">[2.20] 约定编程：架构明显的编程风格</a></li>
</ul>
<h3>工具与实践</h3>
<ul>
<li><a href="/wikis/%5B2.21%5D-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%9F%E4%B8%80%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E7%AE%80%E6%98%8E%E7%89%88%EF%BC%9ACentOs---Nginx---php-fpm---MySql-%5B--Memcached%5D.html">[2.21] 服务器统一部署方案简明版：CentOs + Nginx + php-fpm + MySql [+ Memcached]</a></li>
<li><a href="/wikis/%5B2.22%5D-%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%B2%BE%E7%9B%8A%E9%A1%B9%E7%9B%AE%E5%92%8C%E5%9B%A2%E9%98%9F%E5%BB%BA%E8%AE%BE.html">[2.22] 更多工具：精英项目和团队建设</a></li>
</ul>
<h2>三、扩展类库</h2>
<ul>
<li><a href="/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91.html">[3.1] 扩展类库：微信开发</a></li>
<li><a href="/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81.html">[3.2] 扩展类库：代理模式下phprpc协议的轻松支持</a></li>
<li><a href="/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81.html">[3.3] 扩展类库：基于PHPMailer的邮件发送</a></li>
<li><a href="/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">[3.4] 扩展类库：优酷开放平台接口调用</a></li>
<li><a href="/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">[3.5] 扩展类库：七牛云存储接口调用</a></li>
<li><a href="/wikis/%5B3.6%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">[3.6] 扩展类库：新型计划任务 </a>  </li>
<li>[3.7] 扩展类库：基于Service_Apns的苹果推送 //TODO</li>
<li><a href="/wikis/%5B3.8%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E7%94%A8%E6%88%B7%E3%80%81%E4%BC%9A%E8%AF%9D%E5%92%8C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%BD%95%E9%9B%86%E6%88%90.html">[3.8] 扩展类库：用户、会话和第三方登录集成</a></li>
<li><a href="/wikis/%5B3.9%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9Aswoole%E6%94%AF%E6%8C%81%E4%B8%8B%E7%9A%84%E9%95%BF%E9%93%BE%E6%8E%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E5%AE%9E%E7%8E%B0.html">[3.9] 扩展类库：swoole支持下的长链接和异步任务实现</a>  </li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/wikis/Auth-%E6%9D%83%E9%99%90%E6%89%A9%E5%B1%95%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3">[3.10] 扩展类库：Auth 权限扩展 (由@黄苗笋提供)</a>  </li>
<li><a href="/wikis/%5B3.11%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EFastRoute%E7%9A%84%E5%BF%AB%E9%80%9F%E8%B7%AF%E7%94%B1.html">[3.11] 扩展类库：基于FastRoute的快速路由（支持RESTful）</a>  </li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/Cluster/">[3.12] 扩展类库：基于PhalApi的DB集群拓展Cluster (由@喵了个咪提供)</a>  </li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/Redis/">[3.13] 扩展类库：基于PhalApi的Redis拓展 (由@喵了个咪提供)</a></li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/UCloud/">[3.14] 基于PhalApi的图片上传拓展 (由@Summer)</a></li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/Pay/">[3.15] 基于PhalApi的第三方支付拓展 (由@Summer)</a></li>
<li><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/Image/">[3.16] PhalApi-Image -- 图像处理 (由@喵了个咪提供)</a></li>
</ul>
<h2>四、项目实践</h2>
<ul>
<li>[4.1] 开发实战1：利用扩展库进行微信服务号开发  //TODO</li>
<li><a href="/wikis/%5B4.2%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%982%EF%BC%9A%E6%A8%A1%E6%8B%9F%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91.html">[4.2] 开发实战2：模拟优酷开放平台接口项目开发</a></li>
<li><a href="/wikis/%5B4.3%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%983%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E5%B0%8F%E5%9E%8B%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%EF%BC%88%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%E6%8A%95%E7%A5%A8%E6%B4%BB%E5%8A%A8%EF%BC%89.html">[4.3] 开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）</a></li>
<li>[4.4] 开发实战4：一个可能的大型项目开发（多终端多版本）  //TODO</li>
<li><a href="http://git.oschina.net/xiaoxunzhao/freeApi">[4.5] 落地应用框架：freeApi服务于互联网开发者的免费接口供应平台（由@转身.满目疮痍 提供，并正在致力开发完善中）</a></li>
</ul>
<h2>五、其他</h2>
<ul>
<li><a href="/wikis/%5B5.1%5D-%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%80%9D%E6%83%B3%EF%BC%9APhalApi%E6%A0%B8%E5%BF%83%E8%AE%BE%E8%AE%A1%E5%92%8C%E6%80%9D%E6%83%B3%E8%A7%A3%E8%AF%BB.html">[5.1] 架构与思想：Phal Api核心设计和思想解读</a></li>
<li><a href="/wikis/%5B5.2%5D-%E6%9D%82%E8%B0%88%EF%BC%9A%E6%89%AF%E4%B8%80%E4%BA%9BPhalApi%E7%9A%84%E5%89%8D%E4%B8%96%E5%92%8C%E4%BB%8A%E7%94%9F.html">[5.2] 杂谈：扯一些phal Api的前世和今生</a></li>
<li><a href="/wikis/%5B5.3%5D-%E6%A1%86%E6%9E%B6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%9C%AF%E8%AF%AD%E8%A1%A8%E5%92%8CPHP%E5%BC%80%E5%8F%91%E5%BB%BA%E8%AE%AE.html">[5.3] 框架总结：术语表和PHP开发建议</a></li>
<li><a href="/wikis/%5B5.4%5D-%E8%AE%B8%E5%8F%AF.html">[5.4] 许可</a>  </li>
<li><a href="/wikis/%5B5.5%5D-%E8%81%94%E7%B3%BB%E5%92%8C%E5%8A%A0%E5%85%A5%E6%88%91%E4%BB%AC.html">[5.5] 联系和加入我们</a>  </li>
<li><a href="/wikis/%5B5.6%5D-%E6%9B%B4%E6%96%B0%E6%97%A5%E8%AE%B0.html">[5.6] 更新日记 </a>  </li>
<li><a href="/wikis/%5B5.8%5D-%E8%87%B4%E6%A1%86%E6%9E%B6%E8%B4%A1%E7%8C%AE%E8%80%85%EF%BC%9A%E5%8A%A0%E5%85%A5PhalApi%E5%BC%80%E6%BA%90%E6%8C%87%E5%8D%97.html">[5.8] 致框架贡献者：加入PhalApi开源指南</a> </li>
<li><a href="/wikis/%E9%99%84%E5%BD%951%EF%BC%9A%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E5%8F%82%E8%80%83%E6%A8%A1%E6%9D%BF.html">附录1：接口文档参考模板</a>  </li>
</ul>
<h2>六、SDK包</h2>
<ul>
<li><a href="/wikis/%5B6.1%5D-%E5%9F%BA%E4%BA%8E%E6%8E%A5%E5%8F%A3%E6%9F%A5%E8%AF%A2%E8%AF%AD%E8%A8%80%E7%9A%84SDK%E5%8C%85.html">[6.1] 基于接口查询语言的SDK包</a>  </li>
<li><a href="/wikis/%5B6.2%5D-SDK%E5%8C%85%EF%BC%88JAVA%E7%89%88%EF%BC%89.html">[6.2] SDK包（JAVA版）</a>  </li>
<li><a href="/wikis/%5B6.3%5D-SDK%E5%8C%85%EF%BC%88PHP%E7%89%88%EF%BC%89.html">[6.3] SDK包（PHP版）</a>  </li>
<li><a href="/wikis/%5B6.4%5D-SDK%E5%8C%85%EF%BC%88object-c%E7%89%88%EF%BC%89.html">[6.4] SDK包（object c版） 由@Aevit 提供</a>  </li>
<li><a href="/wikis/%5B6.5%5D-SDK%E5%8C%85%EF%BC%88javascript%E7%89%88%EF%BC%89.html">[6.5] SDK包（javascript版） 由@喵了个咪 提供</a>  </li>
<li><a href="/wikis/%5B6.6%5D-SDK%E5%8C%85%EF%BC%88Ruby%E7%89%88%EF%BC%89.html">[6.6] SDK包（Ruby版）</a>  </li>
</ul>
<h2>七、教程</h2>
<h3>教程-入门篇</h3>
<ul>
<li><a href="/wikis/%5b7.1%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%871(%e7%ae%80%e5%8d%95%e4%bb%8b%e7%bb%8d%e4%bb%a5%e5%8f%8a%e7%8e%af%e5%a2%83%e6%90%ad%e5%bb%ba).html">[7.1]-phalapi-入门篇1(简单介绍以及环境搭建)</a>  </li>
<li><a href="/wikis/%5b7.2%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%872(%e6%8a%8a%e5%ae%83%e7%8e%a9%e8%b5%b7%e6%9d%a5).html">[7.2]-phalapi-入门篇2(把它玩起来)</a>  </li>
<li><a href="/wikis/%5b7.3%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%873(%e8%af%b7%e6%b1%82%e5%92%8c%e8%bf%94%e5%9b%9e).html">[7.3]-phalapi-入门篇3(请求和返回)</a>  </li>
<li><a href="/wikis/%5b7.4%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%874(%e5%9b%bd%e9%99%85%e5%8c%96%e9%ab%98%e5%8f%af%e7%94%a8%e5%92%8c%e8%87%aa%e5%8a%a8%e7%94%9f%e6%88%90%e6%96%87%e6%a1%a3).html">[7.4]-phalapi-入门篇4(国际化高可用和自动生成文档)</a>  </li>
<li><a href="/wikis/%5b7.5%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%875(%e6%95%b0%e6%8d%ae%e5%ba%93%e6%93%8d%e4%bd%9c%e5%92%8cModel%e5%b1%82).html">[7.5]-phalapi-入门篇5(数据库操作和Model层)</a>  </li>
<li><a href="/wikis/%5b7.6%5d-phalapi-%e5%85%a5%e9%97%a8%e7%af%876(%e5%b0%8f%e6%8a%80%e5%b7%a7%e5%92%8c%e6%b5%85%e8%b0%88API%e9%80%82%e7%94%a8%e8%8c%83%e5%9b%b4%e4%bb%a5%e5%8f%8a%e5%85%a5%e9%97%a8%e7%af%87%e6%80%bb%e7%bb%93).html">[7.6]-phalapi-入门篇6(小技巧和浅谈API适用范围以及入门篇总结)</a> </li>
</ul>
<h3>教程-进阶篇</h3>
<ul>
<li><a href="/wikis/%5b7.7%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%871(%e4%b8%89%e5%b1%82%e7%bb%93%e6%9e%84Api%2cDomain%2c%e5%92%8cModel).html">[7.7]-phalapi-进阶篇1(三层结构Api,Domain,和Model)</a>  </li>
<li><a href="/wikis/%5b7.8%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%872(DI%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%92%8c%e5%8d%95%e4%be%8b%e6%a8%a1%e5%bc%8f).html">[7.8]-phalapi-进阶篇2(DI依赖注入和单例模式)</a>  </li>
<li><a href="/wikis/%5b7.9%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%873(%e8%87%aa%e5%8a%a8%e5%8a%a0%e8%bd%bd%e5%92%8c%e6%8b%a6%e6%88%aa%e5%99%a8).html">[7.9]-phalapi-进阶篇3(自动加载和拦截器)</a>  </li>
<li><a href="/wikis/%5b7.10%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%874(notrom%e8%bf%9b%e9%98%b6%e4%bb%a5%e5%8f%8a%e4%ba%8b%e5%8a%a1%e6%93%8d%e4%bd%9c).html">[7.10]-phalapi-进阶篇4(notrom进阶以及事务操作)</a>  </li>
<li><a href="/wikis/%5b7.11%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%875(%e6%95%b0%e6%8d%ae%e5%ba%93%e8%af%bb%e5%86%99%e5%88%86%e7%a6%bb).html">[7.11]-phalapi-进阶篇5(数据库读写分离)</a>  </li>
<li><a href="/wikis/%5b7.12%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%876(%e8%a7%a3%e5%86%b3%e5%a4%a7%e9%87%8f%e6%95%b0%e6%8d%ae%e5%ad%98%e5%82%a8%e6%95%b0%e6%8d%ae%e5%ba%93%e5%88%86%e8%a1%a8%e5%88%86%e5%ba%93%e6%8b%93%e5%b1%95).html">[7.12]-phalapi-进阶篇6(解决大量数据存储数据库分表分库拓展)</a> </li>
<li><a href="/wikis/%5b7.13%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%877(%e4%bd%bf%e7%94%a8%e7%bc%93%e5%ad%98%e4%bb%a5%e5%8f%8a%e7%94%a8redis%e6%8b%93%e5%b1%95%e8%a7%a3%e5%86%b3%e5%ae%9e%e9%99%85%e9%97%ae%e9%a2%98).html">[7.13]-phalapi-进阶篇7(使用缓存以及用redis拓展解决实际问题)</a> </li>
<li><a href="/wikis/%5b7.14%5d-phalapi-%e8%bf%9b%e9%98%b6%e7%af%878(PhalApi%e8%83%bd%e5%b8%a6%e6%9d%a5%e4%bb%80%e4%b9%88%e5%92%8c%e8%bf%9b%e9%98%b6%e7%af%87%e6%80%bb%e7%bb%93).html">[7.14]-phalapi-进阶篇8(PhalApi能带来什么和进阶篇总结)</a> </li>
</ul>
<h2>八、视频教程</h2>
<ul>
<li><a href="/wikis/%5b8.1%5d-PhalApi%e8%a7%86%e9%a2%91%e6%95%99%e7%a8%8b.html">[8.1]-PhalApi视频教程</a>  #接口，从简单开始！
<h4><a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.1%5D-%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85.html">下一章</a></h4></li>
</ul>
<hr />
<p><em>编程于我们，犹如著书而存世。表达，从简单开始；代码，亦然。  -- 暗夜在火星</em>  </p>
<h2>接口，从简单开始！</h2>
<h3>一个隐喻</h3>
<p>假设我们有这样的表达式：</p>
<pre><code>1 + 1 = 2</code></pre>
<p>显然，这是简单明了而易于理解的。但倘若我们这样：</p>
<pre><code>1 + (96 - 867 + 700 - 6 + 7 - 30/10 + 100  - 27) +  1 = 2</code></pre>
<p>同样可以获得相同的结果，但表达却羞涩难懂。  </p>
<p>你可能会偷笑：怎么可能会有人这样编写复杂又没用的代码呢？然而，如果你回顾一下曾经做过的项目或留意一下周围的代码，你会发现，这种情况是真实存在的。  </p>
<p>当然，在不同领域的项目开发中，因各自需求不同各代码的编写也不尽相同。纵使这样，即使我们不能简化到最理想的状态，但至少我们可以通过努力以达到“编写人容易理解的代码”。一如这样：</p>
<pre><code>1 + (0) +  1 = 2</code></pre>
<h3>PhalApi框架所做的</h3>
<p>使用PhalApi框架进行项目开发，我们不能保证项目编写的代码一定会“短而美”，因为更多的代码编写来自于你双手的输入、来自你自己切身的思考和设计。但我们希望PhalApi可以在支持接口快速开发的基础上，为你和更多开发团队提供关于项目开发的一些技艺、参考和帮助。   </p>
<p>所以，与其他很多关注服务器性能的框架不同， <strong>PhalApi更加关注的是人的心情、开发效率和团队合作</strong> ，而这些正是通过测试驱动开发、自动化工具、持续集成和敏捷开发等途径来达成的 。  </p>
<p>然而，这一切一切，都要从代码的编写开始。毕竟我们作为专业人士，代码是我们连接世界的媒介。而接口代码的编写，又应从简单开始。  </p>
<p>泡一杯咖啡，让我们开始吧。  </p>
<h3>本框架和文档适用于</h3>
<ol>
<li>工匠：基于PHP语言的后台接口项目开发，特别是创业项目的快速迭代</li>
<li>探险者：进行定制和扩展，打造成公司内部产品簇的接口应用框架</li>
<li>架构师：专注框架的设计、思想和开源</li>
</ol>
<hr />
<h4><a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.1%5D-%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85.html">下一章</a></h4>
<h1>[1.1]-下载与安装</h1>
<h4><a href="/wikis/%E6%8E%A5%E5%8F%A3%EF%BC%8C%E4%BB%8E%E7%AE%80%E5%8D%95%E5%BC%80%E5%A7%8B%EF%BC%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.2%5D-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE.html">下一章</a></h4>
<hr />
<p><em>我们的时代是一个问题时代，一个问题很大的时代。在科技的神奇影响下，一切都在经历着变革。如果我们愿意在生活中睁大双眼，每天都有问题等着我们去研究，去解决。</em><br />
<em>-- 教皇保罗六世，1969年5月18日 摘自《全球通史-从史前史到21世纪》</em>  </p>
<h2>1.1.1 温馨提示</h2>
<ul>
<li><em>请从release分支拉取稳定的代码</em></li>
<li><em>推荐在Linux服务器上进行开发</em></li>
<li><em>建议PHP &gt;= 5.3.3</em>  </li>
</ul>
<h2>1.1.2 下载</h2>
<p>你可以选择下面你喜欢的方式之一来下载 。</p>
<h3>(1) <a href="http://git.oschina.net/dogstar/PhalApi">网页下载</a></h3>
<p>进入 <a href="http://git.oschina.net/dogstar/PhalApi">Git@OSC - PhalApi</a> ，然后点击下载。<br />
<img src="http://webtools.qiniudn.com/20150411005257_591b373f60b806c454709d805fc959c3" alt="0403" /></p>
<h3>(2)使用git</h3>
<p>命令下，使用git命令快速拉取：  </p>
<pre><code class="language-javascript">$ git clone https://git.oschina.net/dogstar/PhalApi.git</code></pre>
<h2>1.1.3 安装</h2>
<p>安装如同其他的框架一样，将代码上传到到服务器后解压即可。下面是不同环境的参考：</p>
<h3>(1)Windowns系统下的XAMPP</h3>
<p>将PhalApi框架下载后，解压到xampp的htdocs目录下，如：   </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_2d789413b00dc98e6f8cbd5dd77c1776" alt="QQ截图20150403225228" />   </p>
<p>访问：</p>
<pre><code>http://localhost/phalapi/public/demo/</code></pre>
<p>即可看到：
<img src="http://webtools.qiniudn.com/20150411005257_e6805fdf7ef26d2957d10bb8c1b982ee" alt="QQ截图20150403225621" /></p>
<h3>(2)Linux系统下的Nginx</h3>
<p>将PhalApi框架下载并解压到指定目录，假设为： /home/apps/projects/ ，则先配置nginx：  </p>
<pre><code class="language-javascript">server {
    listen 80;
    server_name dev.phalapi.com;
    root /home/apps/projects/PhalApi/Public;
    charset utf-8;
    access_log logs/dev.phalapi.com.access.log;
    error_log logs/dev.phalapi.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}</code></pre>
<p>然后，检测并重启nginx：</p>
<pre><code>$ nginx -t
$ nginx -s reload</code></pre>
<p>在本地绑定HOST：</p>
<pre><code class="language-javascript">#PhalApi小牛试刀
127.0.0.1 dev.phalapi.com</code></pre>
<p>最后，访问默认接口服务： <a href="http://dev.phalapi.com/demo/">http://dev.phalapi.com/demo/</a> ，正常时会返回类如：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1422779027
    },
    "msg": ""
}</code></pre>
<h2>T_FUNCTION错误的处理方案</h2>
<p>如果你当前的PHP环境不支持匿名函数，会报T_FUNCTION这个错误。这时，可这样处理：  </p>
<p>先把匿名函数的写法换成直接new的方式，如：</p>
<pre><code class="language-javascript"> //数据操作 - 基于NotORM
DI()-&gt;notorm = function() {
    $debug = isset($_GET['debug']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};</code></pre>
<p>换成：</p>
<pre><code class="language-javascript"> //数据操作 - 基于NotORM
$debug = isset($_GET['debug']) ? true : false;
DI()-&gt;notorm = new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);</code></pre>
<p>然后，在init.php文件最后添加以下代码：</p>
<pre><code class="language-javascript"> if (!class_exists('Closure')) {
    class Closure {
    }
}</code></pre>
<p>即可解决。  </p>
<p>但由于匿名函数是DI中所需要的重要语言特性，所以建议还是将PHP版本更新到5.3以上。</p>
<hr />
<h4><a href="/wikis/%E6%8E%A5%E5%8F%A3%EF%BC%8C%E4%BB%8E%E7%AE%80%E5%8D%95%E5%BC%80%E5%A7%8B%EF%BC%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.2%5D-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE.html">下一章</a></h4>
<h1>[1.2]-创建一个自己的项目</h1>
<h4><a href="/wikis/%5B1.1%5D-%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85.html">上一章</a>    <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.3%5D-%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C.html">下一章</a></h4>
<hr />
<p><em>有梦想的人学习能力特别强，而且不拒绝任何新东西。 -- 《理想丰满》</em>  </p>
<h2>1.2.1 可视化安装</h2>
<p>将代码上传到服务器并解压后，访问： ./Public/install/ 下的安装向导，如在浏览器打开：  </p>
<pre><code>http://dev.phalapi.com/install/</code></pre>
<p>然后按照提示，一步步操作即可：  </p>
<h3>(0)第零步：同意安装</h3>
<p><img src="http://7xiz2f.com1.z0.glb.clouddn.com/QQ20151024155002.jpg" alt="a pic" />  </p>
<h3>(1)第一步：环境检查</h3>
<p><img src="http://7xiz2f.com1.z0.glb.clouddn.com/QQ20151024155015.jpg" alt="a pic" />  </p>
<h3>(2)第二步：系统配置</h3>
<p><img src="http://7xiz2f.com1.z0.glb.clouddn.com/QQ20151024155030.jpg" alt="a pic" />  </p>
<h3>(3)第三步：接口请求</h3>
<p><img src="http://7xiz2f.com1.z0.glb.clouddn.com/QQ20151024155041.jpg" alt="a pic" /></p>
<h3>(4)运行效果</h3>
<p><img src="http://7qnay5.com1.z0.glb.clouddn.com/step7_20151012223516.png" alt="a pic" /></p>
<h2>1.2.2 手动创建</h2>
<p>在Windows系统下，可以手动创建一个新的项目。  </p>
<p>假设目录为：  F:\xampp\htdocs\PhalApi<br />
以下是简单的创建操作：</p>
<h3>(1)新建项目目录</h3>
<p>在./PhalApi目录下新建一个自己的项目目录，通常以项目名称命名，如项目MyApp对应：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_929b7fdc7e966f54608b25c2e17def29" alt="QQ截图20150404004219" />  </p>
<p>然后，再分别在里面添加接口目录Api、领域目录Domain、数据源目录Model和通用目录Common，即：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_9008ecb3491a703442f3522a86ffd624" alt="QQ截图20150404004458" /></p>
<h3>(2)准备一个接口</h3>
<p>全部的接口类文件需要放置到./Api 目录下，你可以尝试编写一个简单的Default.Index接口，也可以直接将./Demo/Api/Default.php文件复制过来。  </p>
<h3>(3)创建访问入口</h3>
<p>将./Public/demo目录复制一份为对应项目名称小写的目录，如这里的myapp：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_e78908f60d6b34c7f932ba15eff1c1a2" alt="QQ截图20150404004653" /></p>
<p>注意！这里需要修改./Public/myapp/index.php和./Public/myapp/checkApiParams.php中的目录：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_92bff2ed15c72380c54dff46ed800544" alt="QQ截图20150404004731" /></p>
<h3>(4)浏览器访问</h3>
<p>在浏览器输入以下链接：</p>
<pre><code>http://localhost/phalapi/public/myapp/</code></pre>
<p>应该可以看到：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_33f51b3efcd0938bb7fc36bb813f4808" alt="QQ截图20150404004959" /></p>
<h2>1.2.3 自动生成</h2>
<p>在Linux系统下，我们可以使用脚本快速生成一个项目结构，此过程只需要1分钟左右。  </p>
<p>简明操作如下：  </p>
<h3>(1)使用phalapi-buildapp脚本</h3>
<p>phalapi-buildapp脚本用于创建项目，进入 ./PhalApi/PhalApi/ 后，可以看到这个脚本。  </p>
<p>不过，首先，需要为这个脚本添加执行权限：</p>
<pre><code class="language-javascript">$ chmod +x ./phalapi-buildapp</code></pre>
<p>然后，调用脚本创建一个MyApp的项目：</p>
<pre><code class="language-javascript">$ ./phalapi-buildapp MyApp

OK!
</code></pre>
<h3>(2)查看生成的项目及访问</h3>
<p>回到 /path/to/PhalApi 目录，你会发现生成了一个MyApp目录，并且里面的文件如下：  </p>
<pre><code class="language-javascript">$ cd MyApp/
$ tree
.
├── Api
│   └── Default.php
├── Common
├── Domain
├── Model
└── Tests
    ├── Api
    │   └── Api_Default_Test.php
    ├── Common
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php

9 directories, 4 files
</code></pre>
<p>在Public目录下，则会有myapp的访问入口：</p>
<pre><code class="language-javascript">└── myapp
    ├── checkApiParams.php
    └── index.php</code></pre>
<p>在浏览器访问：</p>
<pre><code>#根目录设置为./Public
http://dev.phalapi.com/myapp/

#或没设置根目录为./Public时访问
http://dev.phalapi.com/Public/myapp/</code></pre>
<p>可以看到接口结果输出。</p>
<h3>(3)执行单元测试</h3>
<p>如果安装了PHPUnit，执行一下单元测试，也是通过的！</p>
<pre><code class="language-javascript">$ phpunit ./Tests/
PHPUnit 4.3.4 by Sebastian Bergmann.

...

Time: 7 ms, Memory: 5.75Mb

OK (3 tests, 13 assertions)
</code></pre>
<h2>1.2.4 更多技巧</h2>
<h3>(1)日记目录需要0777权限</h3>
<p>请确保日记目录Runtime拥有写入的权限，可以给0777 。</p>
<h3>(2)脚本加执行权限和软链</h3>
<p>对于脚本命令，需要添加执行权限和软链，便于日后使用：</p>
<pre><code class="language-javascript">$ cd /pah/to/PhalApi/PhalApi

$ chmod +x ./phalapi-buildapp
$ sudo ln -s /path/to/phalapi-buildapp /usr/bin/phalapi-buildapp

$ chmod +x ./phalapi-buildsqls
$ sudo ln -s /path/to/phalapi-buildsqls /usr/bin/phalapi-buildsqls

$ chmod +x ./phalapi-buildtest
$ sudo ln -s /path/to/phalapi-buildtest /usr/bin/phalapi-buildtest</code></pre>
<h3>(3)No such file or directory的解决方案</h3>
<h4>解决方案一：增加 /usr/bin/php软链</h4>
<p>可以将php的执行路径追加到/usr/bin下，即类似：</p>
<pre><code>ln -s /usr/local/php/bin/php /usr/bin/php</code></pre>
<h4>解决方案二：使用alias起别名</h4>
<p>或者通过使用别名 alias 的方式来注册脚本命令：</p>
<pre><code class="language-javascript"># vim /etc/bashrc 

#PhalApi - 自动生成SQL语句
alias phalapi-buildsqls='/usr/local/php/bin/php /path/to/PhalApi/phalapi-buildsqls'</code></pre>
<p>需要注意的是，保存后，需要source一下，即：</p>
<pre><code class="language-javascript"># source /etc/bashrc

$ source /etc/bashrc</code></pre>
<hr />
<h2>1.2.5 四个脚本命令使用说明</h2>
<h3>(1) 创建应用项目</h3>
<p>如前文所述，可以通过<code>phalapi-buildapp</code>脚本来创建一个新的项目，其使用说明如下：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/1_20160422210157.jpg" alt="" />  </p>
<h3>(2)生成单元测试骨架代码</h3>
<p>当需要对某个类进行单元测试时，可使用<code>phalapi-buildtest</code>脚本生成对应的单元测试代码，其使用如下：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/3_20160422210251.jpg" alt="" />  </p>
<p>其中，</p>
<ul>
<li>第一个参数file_path：是待测试的源文件相对/绝对路径  </li>
<li>第二个参数class_name：是待测试的类名  </li>
<li>第三个参数bootstrap：是测试启动文件，通常是./test_env.php文件  </li>
<li>第四个参数author：你的名字，默认是dogstar  </li>
</ul>
<p>通常，可以先写好类名以及相应的接口，然后再使用此脚本生成单元测试骨架代码。<br />
以Default.Index默认接口服务为例，需要为此生成测试骨架代码，可以依次这样操作：  </p>
<pre><code>$ cd /path/to/PhalApi
$ cd ./Demo/Tests
$ ../../PhalApi/phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php &gt; ./Api/Api_Default_Test.php</code></pre>
<p>最后，需要将生成好的代码，重定向保存到你要保存的位置。通常与产品代码对齐，并以“{类名} + _Test.php”方式命名。  </p>
<h3>(3)生成数据库建表SQL</h3>
<p>当需要创建数据库表时，可以使用<code>phalapi-buildsqls</code>脚本结合配置文件dbs.php生成建表SQL，这个工具在创建分表时尤其有用，其使用如下：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/2_20160422210230.jpg" alt="" />  </p>
<p>其中，</p>
<ul>
<li>第一个参数dbs_config：是指向数据库配置文件./Config/dbs.php的路径，可以使用相对路径  </li>
<li>第二个参数table：是需要创建sql的表名，每次生成只支持一个  </li>
<li>第三个参数engine：（可选）是指数据库表的引擎，可以是：Innodb或者MyISAM  </li>
</ul>
<blockquote>
<p>温馨提示：需要提前先将建表的SQL语句（除主键id和ext_data字段外）放置到./Data/目录下，文件名为：{表名}.sql。  </p>
</blockquote>
<p>如，我们需要生成10用户user_session表的的建表语句，那么需要添加数据文件./Data/user_session.sql（除主键id和ext_data字段外）：  </p>
<pre><code>      `user_id` bigint(20) DEFAULT '0' COMMENT '用户id',
      `token` varchar(64) DEFAULT '' COMMENT '登录token',
      `client` varchar(32) DEFAULT '' COMMENT '客户端来源',
      `times` int(6) DEFAULT '0' COMMENT '登录次数',
      `login_time` int(11) DEFAULT '0' COMMENT '登录时间',
      `expires_time` int(11) DEFAULT '0' COMMENT '过期时间',</code></pre>
<p>然后，进入到项目根目录，执行命令：  </p>
<pre><code>$ php ./PhalApi/phalapi-buildsqls ./Config/dbs.php user_session</code></pre>
<p>就会看到生成好的SQL语句了，类似：  </p>
<pre><code>CREATE TABLE `phalapi_user_session_0` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_1` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_2` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_3` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_4` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_5` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_6` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_7` (
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_8` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_user_session_9` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      ... ...
      `ext_data` text COMMENT 'json data here',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre>
<p>最后，便可把生成好的SQL语句，导入到数据库，进行建表操作。</p>
<h3>(4)生成接口代码 - V1.3.4及以上版本支持</h3>
<p>当需要编写开发一个接口时，可以使用<code>phalapi-buildcode</code>脚本生成基本的Api、Domain和Model代码。此脚本不是很强悍的，项目可以根据自己的喜欢使用，或者修改定制自己的模板。其使用如下：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/4_20160514155143.png" alt="" />  </p>
<p>其中，</p>
<ul>
<li>第一个参数app_path：是指项目根目录到你的项目的相对路径  </li>
<li>第二个参数api_path：是需要创建接口的相对项目的相对路径，支持多级目录，可不带.php后缀  </li>
<li>第三个参数author：（可选）你的名字，默认为空</li>
<li>第四个参数overwrite：（可选）是否覆盖已有的代码文件，默认为否</li>
</ul>
<p>例如，我们要为Demo项目生成一个新的接口文件./AA/BB/CC.php，则可以：  </p>
<pre><code>$ cd /path/to/PhalApi
$ ./PhalApi/phalapi-buildcode Demo AA/BB/CC dogstar
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB ...
Start to create folder /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Api/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Domain/AA/BB/CC.php ...
Start to create file /mnt/hgfs/F/PHP/PhalApi/PhalApi/../Demo/Model/AA/BB/CC.php ...

OK! AA/BB/CC has been created successfully!
</code></pre>
<p>可以看到生成的代码有：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/cc20160514155950.png" alt="" />   </p>
<p>访问接口：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/aa20160514160328.png" alt="" />  </p>
<p>最后，在线接口列表，可以看到：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/bb20160514160158.png" alt="" />  </p>
<h4><a href="/wikis/%5B1.1%5D-%E4%B8%8B%E8%BD%BD%E4%B8%8E%E5%AE%89%E8%A3%85.html">上一章</a>    <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.3%5D-%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C.html">下一章</a>#[1.3]-在线体验</h4>
<h4><a href="/wikis/%5B1.2%5D-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.4%5D-%E6%96%87%E6%A1%A3%E3%80%81%E5%B8%AE%E5%8A%A9%E5%92%8C%E5%AE%98%E7%BD%91.html">下一章</a></h4>
<hr />
<p><em>片刻之欢愉，不如须臾之宁静。</em>  </p>
<h2>1.3.1 在线体验</h2>
<pre><code class="language-javascript">//默认的接口服务
http://demo.phalapi.net/
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278491
    },
    "msg": ""
}

//带参数的示例接口
http://demo.phalapi.net/?service=Default.Index&amp;username=oschina
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "oschina您好，欢迎使用PhalApi！",
        "version": "1.2.0",
        "time": 1443278475
    },
    "msg": ""
}

//故意请求一个非法的服务
http://demo.phalapi.net/?service=Demo.None
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：服务Demo.None不存在"
}</code></pre>
<h2>1.3.2 在线接口调试</h2>
<p>这里说的接口调试，更多是针对接口调用过程中所执行的SQL语句查看，如：  </p>
<pre><code class="language-javascript">//带&amp;__sql__=1可打印SQL语句
http://demo.phalapi.net/?service=User.getMultiBaseInfo&amp;user_ids=1,2,3&amp;__sql__=1

返回：
[1 - 0.00032s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
[2 - 0.00019s]SELECT * FROM tbl_user WHERE (id = ?); -- 2
[3 - 0.00016s]SELECT * FROM tbl_user WHERE (id = ?); -- 3
{"ret":200,"data":{"code":0,"msg":"","list":[{"id":"1","name":"dogstar","note":"oschina"},{"id":"2","name":"aevit","note":"fami"},{"id":"3","name":"test","note":"phalapi"}]},"msg":""}</code></pre>
<h2>1.3.3 [酷！]接口参数在线查询</h2>
<p>为了方便客户端查看最新的接口参数，特别提供此在线工具，根据接口代码实时生成接口参数报表，完全不需要后台开发编写维护额外的文档。我觉得，这很符合敏捷开发之道。</p>
<pre><code>//接口参数在线查询工具链接
http://demo.phalapi.net/checkApiParams.php</code></pre>
<p>如：<a href="http://demo.phalapi.net/checkApiParams.php">http://demo.phalapi.net/checkApiParams.php</a> ，访问效果如下：  </p>
<p><img src="http://7qnay5.com1.z0.glb.clouddn.com/20150926-2.png" alt="mahua" />  </p>
<p>因此，接口所需要的参数，对于接口开发人员，也只是简单配置一下参数规则，便可以轻松获取。<br />
关于具体的参数规则配置和接口结果返回格式定义，后期会详细讲到。  </p>
<hr />
<h4><a href="/wikis/%5B1.2%5D-%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%A1%B9%E7%9B%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.4%5D-%E6%96%87%E6%A1%A3%E3%80%81%E5%B8%AE%E5%8A%A9%E5%92%8C%E5%AE%98%E7%BD%91.html">下一章</a>#[1.4]-文档、帮助和官网</h4>
<h4><a href="/wikis/%5B1.3%5D-%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.10%5D-%E5%AF%B9PhalApi%E6%A1%86%E6%9E%B6%E7%9A%84%E6%8A%89%E6%8B%A9.html">下一章</a></h4>
<hr />
<p><em>创造性源于专注并且持久的思考，直至灵感来临。 -- 《快速软件开发(珍藏版)》</em>  </p>
<h2>1.4.1 <a href="http://www.phalapi.net/wikis/">开发文档</a></h2>
<p>开发文档，是由PhalApi团队精心编写的，意在给广大开发同学提供开发指导和使用帮助。最全和最新的开发文档，请查看这里：</p>
<pre><code>http://www.phalapi.net/wikis/</code></pre>
<h2>1.4.2 <a href="http://www.phalapi.net/docs/">在线类参考手册</a></h2>
<p>此部分在线类参考手册，是使用phpdoc自动生成，支持移动端访问：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_3b957a17253d197b8580074a54fbb4c0" alt="1630583589" />  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_df91fd36ee7ccd2b86aacb59c7935934" alt="1630583592" /></p>
<h2>1.4.3 <a href="http://git.oschina.net/dogstar/PhalApi/issues">问题与反馈</a></h2>
<p>我们建议使用issue来纪录来共享问题，你也可以通过其他方式来与我们进行沟通，以获得更多帮助。</p>
<h2>1.4.4 <a href="http://www.phalapi.net/">官网</a></h2>
<p>官网链接为：</p>
<pre><code>http://www.phalapi.net/</code></pre>
<p>支持移动端显示：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_3aee9a38e616fd4e6e4cf3ea66245223" alt="1630583591" /></p>
<h2>1.4.4 框架升级</h2>
<p>最后，补充说明一下，大家较为关心的框架升级问题。  </p>
<p>由于PhalApi框架的核心代码，已经在前期有意识地单独隔离了出来，即在 ./PhalApi/PhalApi 目录下。  </p>
<p>所以，当后期项目需要升级框架时，直接替换此目录下全部的文件即可。为了保险起见，在升级前，你可以做好备份工作。<br />
我们会争取100%做到框架的向前兼容。如果升级后有什么 问题，可随时与我们联系。  </p>
<p>对此，项目不应该直接针对核心部分进行改动，而是在其他地方放置，以免影响框架升级。</p>
<hr />
<h4><a href="/wikis/%5B1.3%5D-%E5%9C%A8%E7%BA%BF%E4%BD%93%E9%AA%8C.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.10%5D-%E5%AF%B9PhalApi%E6%A1%86%E6%9E%B6%E7%9A%84%E6%8A%89%E6%8B%A9.html">下一章</a>#[1.10]-对PhalApi框架的抉择</h4>
<h4><a href="/wikis/%5B1.4%5D-%E6%96%87%E6%A1%A3%E3%80%81%E5%B8%AE%E5%8A%A9%E5%92%8C%E5%AE%98%E7%BD%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.11%5D-%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%28backup%29.html">下一章</a></h4>
<hr />
<p><em>人类一思考，上帝就偷笑了。  --  《下一个倒下的会不会是华为》</em></p>
<h2>1.10.1 对PhalApi框架的抉择</h2>
<p>能使用框架来进行项目开发，和知道为何使用此框架进行项目开发明显不同。  </p>
<p>对框架的选择，名义上是架构师的职责，但对于充满好奇心和有着更强求知欲的开发人员来说，同样应该给予关注。  </p>
<p>之所以选择一个框架进行项目开发，明显可以减少很多不必要的重复编码，但更深层次，则是可以减少开发周期、统一开发规范、降低项目风险，不致于项目失控。  </p>
<p>所以，在决定使用PhalApi框架前，对于此框架是否适用于即将启动的项目进行一番思索，是大有裨益的。通过 <strong>推定框架</strong> ，有助于避免因采用不当的框架而对项目造成不必要的阻力。  </p>
<h3>1、适用的场景和项目</h3>
<h4>海量数据和移动App</h4>
<p>此框架特别适用于现在各种 <strong>移动App</strong>项目的后台接口开发，以及服务器间的后台接口开发，同时具有应对 <strong>海量数据</strong> 的能力。并且还可以挂靠多个项目，也可以很好地支持多个终端、开放不同的入口。    </p>
<p>同时提供了可以很好应对海量数据的解决方案：如没有提供图片上传的代码或工具而希望开发人员将图片上传到CDN，支持大数据的存储，以及后台计划任务。  </p>
<h4>HTTP协议和JSON格式</h4>
<p>基于此，我们采用了主流设计，即将框架设计成使用HTTP协议并JSON格式返回结果的接口请求，因为这能为大众熟悉并潜意识接受。
通过规范的接口调用和返回，有利于客户端和后台开发人员的关系融洽。</p>
<h3>2、敏捷开发和快速交付</h3>
<p>虽然框架和工程实践间没有必然的联系，但这其中有一些微妙的关系。  </p>
<p><strong>一种代码编写的方式会形成一种开发的风格；一种开发风格会奠定一个团队的合作氛围；一个团队的合作氛围会决定项目交付的质量。</strong></p>
<p>而PhalApi所提倡和希望做到的正是通过让后台接口开发更简单以让后台开发人员心情更愉悦，从而为客户端提供高质量稳定的接口以支撑更多优质App的开发。并且，结合重构、测试驱动开发和持续集成等，可以让你的项目如虎添翼，在快速交付的同时，体验编码开发的乐趣。  </p>
<h3>3、约束和关注</h3>
<p><strong>架构约束程序。</strong>  </p>
<p>约束有时会让开发者处处受阻。但好的约束能够统一规范而不致于项目代码凌乱不堪，也不会轻易地允许低级开发新手犯下一些本可避免的BUG。正如，我们都讨厌等待红绿灯，但我们必须肯定它对交通和生命安全保障的作用。  </p>
<h3>4、复杂领域业务的应对和解决方案</h3>
<p>正如前面说到的，我们关注在海量数据下为移动App提供稳定的接口，我们提倡敏捷开发下的快速交付。所以我们去掉与接口开发无关的功能，没有提供视图渲染和模板解析的操作。  </p>
<p>但只是这样而已吗？  </p>
<p>不！我们还关注对复杂领域业务的应对和解决方案。  </p>
<p>如果我们PhalApi框架所关注的，也是你们项目所关注的，那么我们有理由相信PhalApi能为你的项目带来很多友好的约束和贴心的帮助。</p>
<h3>5、框架的性能</h3>
<p>很多框架都要强调其能提供的性能，然后PHP本身就是动态的脚本语言，要想提高项目的运行速度，就是要进行减法，即减少不必要的PHP代码。但是前面强调性能的框架则做了与其承诺矛盾的做法：为框架增添了很多项目可能不需要用到的功能。明显地就是一系列既定的执行流程和侦听事件、回调、调度等等。<br />
而这些，势必会对性能有所影响，特别当应用项目不需要这些特性却又不能定制简化时，框架所谓的强悍功能会适得其反。  </p>
<p>对于这一块，我们则提供了极大的空间。因为，入口的文件，由你指定。除了一些必要的加载外，很多都可以支持自定义和定制化。  </p>
<p>而且我们也使用autobench进行了压力测试和通过xhprof进行了性能剖析，证明PhalApi框架在性能上确实如我们预料的那样 -- 快！  </p>
<h3>6、PhalApi的成熟度、部署与学习成本</h3>
<h4>成就度</h4>
<p>不可否认，PhalApi还是太年轻了。  </p>
<p>PhalApi正式开源于2015年1月，但是我们在努力完善中并致力于有助于快速开发的后台接口。我们“减少不必要的创新”，我们坚持恒星一般的接口并很好地支持框架升级， 我们尽量提供优秀的文档和及时的沟通帮助。  </p>
<p>更为重要的是，这是一个不只为框架而框架的PhalApi框架。 </p>
<p>如果在开发过程中对所遇到的问题而框架不能很好解决和支持时，可以考虑改写框架，将某一目标或属性提升至架构，其他团队成员则无须再编写重复代码去重复实现。因为，你会发现，在PhalApi下，你可以轻松做到这一点： <strong>小步快跑下的浮现式设计</strong> 。</p>
<h4>部署</h4>
<p>在Nginx下，你可以快速部署此框架。  </p>
<p>以下是为初学者参考的Nginx配置：  </p>
<pre><code class="language-javascript">server {
    listen       80;
    server_name  demo.phalapi.com;

    root   /home/apps/projects/demo.phalapi.com/Public;

    charset utf-8;

    access_log  logs/demo.phalapi.com.access.log;
    error_log  logs/demo.phalapi.com.error.log;

    location / {
        index  index.html index.htm index.php;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}</code></pre>
<p>除此之前，我们已经对框架的结构目录进行了很好地划分。包括配置文件、日记文件、框架代码、多个项目挂靠、不同的入口开放以及多App的支持等等。  </p>
<p>在结合Phing进行一键发布时，你会发现，项目的部署发布同样方便快捷。</p>
<h4>学习成本</h4>
<p>这块学习成本是不可避免的，毕竟PhalApi是一个新的框架，很多开发同学之前都没有接触过。  </p>
<p>但我们尽量站在后台开发同学的角度，用心地设计每一个接口，编写每一行代码。在源代码里面倾注了必要的注释和使用示例，并同步提供了详细的文档说明和实战代码，以便快速入门上手。  </p>
<p>更为重要的是，我们都是尽可能按照主流惯例来设计接口规范，以便各位同学可以快速上手，就仿佛这个框架是出自你的设计一样。  </p>
<h2>1.10.2 何时可以采用PhalApi？</h2>
<p>我们致力于提供可以快速开发后台接口的框架，并希望能对你的项目有实际的帮助。所以，我们不会鼓吹PhalApi框架，因为没有可以适用于任何领域、任何实际项目的框架。也就是没有所谓的银弹，也没有一种放之四海而皆准的方法，起码你可能需要作一些调整。  </p>
<p>而且，我们也不会赞同你不加考虑就采用PhalApi框架，因为如果此框架不利于你项目的开发，甚至有反作用，这显然有悖于我们的初心：为你的项目提供实质的帮助。因此，在采用PhalApi框架前，你应该稍微查看一下我们的文档，和相关的源代码。以下是一些初步的建议。  </p>
<pre><code> 1、架构无关：如果你的只是小项目，无需要过多考虑涉及大规模决策、各模块及元素、属性间的微妙关系，即架构无关时。
PhalApi和其他框架一样，也能满足小项目的开发需求。  

 2、专注架构：如果你需要专注框架，则更需要理解你项目将面临的约束、风险和需要满足的质量属性。
然后评估PhalApi是否能统一这此约束、降低风险并满足这些质量属性。若能，则用之；若不能，则另取之。  

 3、提升架构：最后，如果你发现PhalApi拥有不断演进的能力并有助于浮现式设计（我们也正是如此设计和研发的），并且愿意
将PhalApi框架提升到你们产品线的后台接口框架，那么你将能为你的团队、项目乃至公司提供一个更优质的框架，并会为之感到兴奋和骄傲。
当然，“能力越大，责任也就越大”。但我们作为程序员，不正是希望能以已之所学，服务于人吗？</code></pre>
<h2>1.10.3 何时不应该使用PhalApi？</h2>
<p>正如其他负责任的开源框架一样，我们在说明PhlaApi框架适用场景的同时，也应该指明不应该使用PhalApi的时机。其中包括但不限于：  </p>
<ul>
<li>需要开发CLI项目时（但已提供支持命令行项目开发的<a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/CLI">CLI扩展类库</a>）；</li>
<li>需要开发网站项目，即有界面展示和视图渲染（但已提供支持视图渲染的<a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/View">View扩展类库</a>）；</li>
<li>对数据严谨性要求高，如金融行业的相关项目，毕竟PHP是弱类型语言；</li>
</ul>
<hr />
<h4><a href="/wikis/%5B1.4%5D-%E6%96%87%E6%A1%A3%E3%80%81%E5%B8%AE%E5%8A%A9%E5%92%8C%E5%AE%98%E7%BD%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.11%5D-%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%28backup%29.html">下一章</a>#[1.11]-快速入门(backup)</h4>
<h4><a href="/wikis/%5B1.10%5D-%E5%AF%B9PhalApi%E6%A1%86%E6%9E%B6%E7%9A%84%E6%8A%89%E6%8B%A9.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.12%5D-%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE.html">下一章</a></h4>
<hr />
<p><em>一个真正的强者，不是摆平了多少人，而是他能帮助到多少人。  --开源中国源创会分享广州站 @海洋之心-悟空</em>  </p>
<hr />
<h2>1.11.1 模拟开发：获取开源中国用户信息接口</h2>
<p>首次使用此接口开发框架时，可以先查看此开发示例。  </p>
<p>假设，我们需要为开源中国打造一个平放平台，其中有一个接口是可以根据用户ID来获取用户的基本信息。  </p>
<p>本文，就以模拟获取开源中国用户信息接口开发（即：从数据库获取用户的基本信息并以JSON格式返回给客户端）为例，简明的接口开发中的流程，以及用到的基本功能和操作，主要包括有：统一入口文件、参数规则配置、接口层/领域层/模型持久层、日记纪录、数据库操作、配置读取、国际化翻译等。  </p>
<p>以给大家一个感观的认识。  </p>
<p>最终接口的调用与返回结果如下：  </p>
<pre><code class="language-javascript">//接口请求格式
http://dev.phalapi.com/demo/?service=User.GetBaseInfo&amp;user_id=帐号ID

//返回结果格式
{
    "ret": 200,
    "data": {           
        "code": 0,    //状态码，0表示正常获取，1表示用户不存在
        "msg": "",
        "info": {      //用户信息
            "id": "1",    //用户ID
            "name": "dogstar",   //帐号
            "note": "oschina"   //来源
        }
    },
    "msg": ""
}</code></pre>
<h2>1.11.2 开发流程</h2>
<h3>(1) 统一入口文件</h3>
<p>为了更好保护我们的项目代码，建议将接口的访问路径设置在：./PhalApi/Pubic目录，并且各套接口（如按版本分：v1/v2/v3等等；按不同终端分：ios/android/pc等）各自独立入口。所以本示例中将./PhalApi/Pubic/demo/index.php下。  </p>
<p>参数入口文件的写法，我们可以快速得到基本的入口文件如下：  </p>
<pre><code class="language-javascript">// $ vim ./Public/demo/index.php 

&lt;?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()-&gt;loader-&gt;addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$server = new PhalApi();
$rs = $server-&gt;response();
$rs-&gt;output();</code></pre>
<p>此外，我们还需要一个公共的初始化文件：  </p>
<pre><code class="language-javascript">//$ vim ./Public/init.php 

&lt;?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&amp;初始化服务组件 ---------------- **/

//自动加载
DI()-&gt;loader = $loader;

//配置
DI()-&gt;config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', 
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()-&gt;notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()-&gt;debug = !empty($_GET['__debug__']) ? true : DI()-&gt;config-&gt;get('sys.debug');

//翻译语言包设定
SL('zh_cn');
</code></pre>
<h3>(2)TDD测试驱动开发</h3>
<p>遵循最佳实践，我们在编写代码前先编写单元测试。但同时为了减少编写测试代码的痛苦，我们可以先定义接口函数签名，再通过脚本自动生成测试代码骨架来提高我们的开发效率。  </p>
<pre><code class="language-javascript">//$vim ./Demo/Api/User.php

&lt;?php

class Api_User extends PhalApi_Api
{   
    public function getBaseInfo()
    {
    }
}</code></pre>
<p>通过脚本生成测试骨架：  </p>
<pre><code class="language-javascript">$ mkdir -p ./Demo/Tests/Api/
$ cd ./Demo/Tests/Api
$ php ../../../PhalApi/build_phpunit_test_tpl.php ../../Api/User.php Api_User ./../test_env.php</code></pre>
<p>然后，根据/Public/demo/index.php入口文件，也搭建一个测试环境的入口文件：  </p>
<pre><code class="language-javascript">vim ./Demo/Tests/test_env.php</code></pre>
<p>修正一下Api_Examples_User_Test.php里，测试环境test_env.php的包含路径：  </p>
<pre><code class="language-javascript">//手动调用test_env.php的路径
require_once dirname(__FILE__) . '/../test_env.php';</code></pre>
<p>并且，修改测试，以符合我们通过userId=1获取基本信息（名字为dogstar，来源为oschina）：</p>
<pre><code class="language-javascript">//$vim ./Demo/Tests/Api/Api_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $str = 'service=User.GetBaseInfo&amp;user_id=1';
        parse_str($str, $params);

        DI()-&gt;request = new PhalApi_Request($params);

        $api = new Api_User(); 
        //自己进行初始化
        $api-&gt;init();
        $rs = $api-&gt;getBaseInfo();

        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('msg', $rs);
        $this-&gt;assertArrayHasKey('info', $rs);

        $this-&gt;assertEquals(0, $rs['code']);

        $this-&gt;assertEquals('dogstar', $rs['info']['name']);
        $this-&gt;assertEquals('oschina', $rs['info']['note']);
    }</code></pre>
<p>此时，单元测试是预期失败的：  </p>
<pre><code class="language-javascript">$ phpunit ./Api_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

F

Time: 3 ms, Memory: 5.25Mb

There was 1 failure:

1) PhpUnderControl_ApiUser_Test::testGetBaseInfo
Failed asserting that a NULL is not empty.

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.</code></pre>
<h3>(3)Api接口层</h3>
<p>此接口层，主要是负责响应客户端的请求，调用需要的领域层进行必要的服务功能提供。</p>
<h3>配置参数规则</h3>
<p>为了获取到用户ID，我们可以在getRules()函数里面定义参数规则，以便框架自动帮我们过滤获取需要的参数。</p>
<blockquote>
<p>温馨提示：<br />
接口层的全部类成员函数都应以小写开头。<br />
但对外，函数首字母不区分大小写，因为框架会将请求的函数强制转换成小写再执行。  </p>
<p>原因在于：<br />
1、我们坚持驼峰法的代码风格；<br />
2、对外界我们可以统一使用大写来提供服务名称，如：User.Login，这样更显专业。  </p>
</blockquote>
<pre><code class="language-javascript">//$vim ./Demo/Api/User.php

&lt;?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'getBaseInfo' =&gt; array(
                'userId' =&gt; array('name' =&gt; 'user_id', 'type' =&gt; 'int', 'min' =&gt; 1, 'require' =&gt; true),
            ),
        );
    }

    //...</code></pre>
<p>如上，我们就定义了getBaseInfo接口中的user_id参数，对应的变量名字为userId，整型，最小值为1，必须。</p>
<h4>接口实现</h4>
<pre><code class="language-javascript">//$vim ./Demo/Api/User.php

    public function getBaseInfo()
    {
        $rs = array('code' =&gt; 0, 'msg' =&gt; '', 'info' =&gt; array());

        $domain = new Domain_User();
        $info = $domain-&gt;getBaseInfo($this-&gt;userId);

        if (empty($info)) {
            DI()-&gt;logger-&gt;debug('user not found', $this-&gt;userId);

            $rs['code'] = 1;
            $rs['msg'] = T('user not exists');
            return $rs;
        }

        $rs['info'] = $info;

        return $rs;
    }</code></pre>
<h3>(4)Domain领域层</h3>
<p>领域层主要是关注复杂业务的处理，以及缓存的处理、耗时操作后台异步处理等，并调用Model持久层获取需要的数据。因此，是Api与Model层之间的桥梁。  </p>
<p>在此示例中，我们只需要简单地调用Model层获取用户的信息即可，再加强一下用户ID的合法性判断。  </p>
<pre><code class="language-javascript">//$ vim ./Demo/Domain/User.php 

&lt;?php

class Domain_User
{
    public function getBaseInfo($userId)
    {
        $rs = array();

        $userId = intval($userId);
        if ($userId &lt;= 0) {
            return $rs;
        }

        $model = new Model_User();
        $rs = $model-&gt;getByUserId($userId);

        return $rs;
    }
}</code></pre>
<h3>(5)Model持久层</h3>
<p>此一层主要关注数据从持久存储的获取，特别是针对数据库的操作，但不排除其他媒介，如文件、缓存等。  </p>
<p>首先，先准备一下我们需要的表和数据：</p>
<pre><code class="language-javascript">CREATE TABLE `phalapi_test`.`tbl_user` (
  `id` INT NOT NULL,
  `name` VARCHAR(45) NULL,
  `note` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

INSERT INTO `phalapi_test`.`tbl_user` (`id`, `name`, `note`) VALUES ('1', 'dogstar', 'oschina');</code></pre>
<p>然后，编写需要的Model代码，即利用NotORm实现对数据的操作：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Model/User.php 

&lt;?php

class Model_User
{
    public function getByUserId($userId)
    {
        return DI()-&gt;notorm-&gt;user-&gt;select('*')-&gt;where('id = ?', $userId)-&gt;fetch();
    }
}</code></pre>
<h3>(6)单元测试通过啦！</h3>
<p>在完成上面简单的开发后，我们即可以实现接口的开发，运行一下刚才的单元测试，完美通过！</p>
<pre><code class="language-javascript">$ phpunit ./Api_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

SELECT * FROM tbl_user WHERE (id = ?); -- 1

.SELECT * FROM tbl_user WHERE (id = ?); -- 1&lt;br /&gt;

Time: 34 ms, Memory: 6.50Mb

OK (2 tests, 7 assertions)</code></pre>
<p>在单元测试的保证下，我们便可以放心大胆地将我们的代码发布到外网，提供给更多的开发者，和终端用户使用。  </p>
<h3>(7)数据库配置</h3>
<p>因为是单元测试，所以我们配置搭建了新的一个测试环境，特别对于数据库的配置，如下：</p>
<pre><code class="language-javascript">//$ vim ./Config/dbs.php

&lt;?php
/**
 * examples配置
 */

return array(
    /**
     * DB数据库服务器集群
     */
    'servers' =&gt; array(
        'db_demo' =&gt; array(
            'host'      =&gt; '192.168.0.104',         //数据库域名
            'name'      =&gt; 'phalapi_test',          //数据库名字
            'user'      =&gt; 'root',                  //数据库用户名
            'password'  =&gt; '123456',                //数据库密码
            'port'      =&gt; '3306',                  //数据库端口
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' =&gt; array(
        '__default__' =&gt; array(
            'prefix' =&gt; 'tbl_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
            ),
        ),
    ),
);</code></pre>
<blockquote>
<p>温馨提示：<br />
为了方便在单元测试时进行调试，和查看日记，对于全部查询、执行的SQL语句都会显示出来，全部的日记改用控制台输出。    </p>
</blockquote>
<h3>(8)最终接口调用</h3>
<p>接口调用的链接，这时已经相当明了了，即：域名 + 路径（/demo） + 参数（可从单元测试那直接获取）</p>
<p>如本示例的是：</p>
<pre><code class="language-javascript">http://dev.phalapi.com/demo/?service=User.GetBaseInfo&amp;user_id=1</code></pre>
<p>返回的结果是：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "info": {
            "id": "1",
            "name": "dogstar",
            "note": "oschina"
        }
    },
    "msg": ""
}</code></pre>
<p>截图效果：<br />
<img src="http://webtools.qiniudn.com/20150411005257_a5d0a984e4dd7fcdd1c95026e337003d" alt="show" />  </p>
<blockquote>
<p>温馨提示：<br />
如果提示日记写入失败，请确保./Runtime目录具有写入权限，即0777。  </p>
</blockquote>
<h2>1.11.3 更多简明的使用</h2>
<h3>(1)日记纪录</h3>
<p>当我们访问一个不存在的用户时，将会触发日记纪录：</p>
<pre><code class="language-javascript">DI()-&gt;logger-&gt;debug('user not found', $this-&gt;userId);</code></pre>
<p>如访问：</p>
<pre><code>http://dev.phalapi.com/demo/?service=User.GetBaseInfo&amp;user_id=2</code></pre>
<p>然后，可以在Runtime下看到按天分目录的日记：</p>
<pre><code class="language-javascript">$ tailf ./Runtime/log/201501/20150128.log 
2015-01-28 00:37:34|DEBUG|user not found|2</code></pre>
<blockquote>
<p>温馨提示：<br />
外网环境上，请把Runtime目录软链到磁盘空间很大的路径。  </p>
</blockquote>
<h3>(2)国际化翻译</h3>
<p>当需要翻译时，可以使用人性化的函数T()，如：</p>
<pre><code class="language-javascript">$rs['msg'] = T('user not exists');</code></pre>
<p>对应地需要补充翻译的内容：</p>
<pre><code class="language-javascript">//$ vim ./Language/zh_cn/common.php 

    'user not exists' =&gt; '用户不存在',</code></pre>
<p>还是以上面的用户不存在为例，看下运行的截图效果：
<img src="http://webtools.qiniudn.com/20150411005257_d8fc7091e4a2717fa9e1b4f9891117ff" alt="show" /></p>
<h3>(3)配置读取</h3>
<p>配置的读取，使用方便，直接通过以下方式便可以获取，以点号分割：</p>
<pre><code class="language-javascript">DI()-&gt;config-&gt;get('dbs')</code></pre>
<p>第一段，必须为文件名，后面的为用点号相连的数组下标，不限级。</p>
<h3>(4)[酷！]接口参数在线查询</h3>
<p>为了方便客户端实时查看最新的接口参数，这里提供了一个快速的在线工具：</p>
<pre><code class="language-javascript">http://demo.phalapi.net/checkApiParams.php?service=User.GetBaseInfo</code></pre>
<p>用浏览器打开后，即可以看到最新的接口参数说明， 不需要后台接口开发人员编写文档维护，直接从代码中生成参数报表：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_af3f58d9bc050b4906a57fea3d04e9b2" alt="show" /></p>
<h2>1.11.4 Demo相关代码文件</h2>
<p>从上面可以得到，此示例相关的代码如下：</p>
<pre><code class="language-javascript">Demo$ tree
.
├── Api
│   ├── Default.php
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php
└── Tests
    ├── Api
    │   ├── Api_Default_Test.php
    │   └── Api_User_Test.php
    └── test_env.php</code></pre>
<hr />
<h4><a href="/wikis/%5B1.10%5D-%E5%AF%B9PhalApi%E6%A1%86%E6%9E%B6%E7%9A%84%E6%8A%89%E6%8B%A9.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.12%5D-%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE.html">下一章</a></h4>
<h1>[1.12]-参数规则：接口参数规则配置</h1>
<h4><a href="/wikis/%5B1.11%5D-%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%28backup%29.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">下一章</a></h4>
<hr />
<p><em>“播下一种思想，收获一种行为；播下一种行为，收获一种习惯；播下一种习惯，收获一种性格；播下一种性格，收获一种命运。”  --《成君忆：水煮三国》</em>  </p>
<h2>1.12.1 参数解析</h2>
<p>参数，对于接口来说，是非常重要的输入。对于外部调用来说，同等重要。</p>
<p>因此，对于参数这块，我们是希望能够既减轻后台开发对接口参数获取、判断、验证、文档编写的痛苦；又便于客户端方便的、自由的调用；既利已又利他。</p>
<p>由此，我们引入了 <strong>参数解析</strong> 这一概念，即：通过配置参数的规则，即可自动实现参数的获取和验证。</p>
<h2>1.12.2 参数解析的配置规则</h2>
<p>熟悉Yii的同学，对于以下的规则配置应该倍感亲切，但是不熟悉的同学也可以同样快速上手。因为，你会慢慢发现，这样的规则很符合我们PHP开发的规范，如果没有，我们继续努力改进。</p>
<p>格式如下：</p>
<pre><code class="language-javascript">array(
    '参数名' =&gt; array('name' =&gt; '接口参数名称', 'type' =&gt; '类型', 'default' =&gt; '默认值', ...),
    ... ...
)</code></pre>
<h2>1.12.3 示例</h2>
<h3>(1)简单的示例</h3>
<p>假设这样的业务场景，我们需要提供一个用户登录的接口，其中需要用户名和密码，因此：</p>
<pre><code class="language-javascript">&lt;?php

class Api_User extends PhalApi_Api
{
    public function getRules()
    {
        return array(
            'login' =&gt; array(
                'username' =&gt; array('name' =&gt; 'username'),
                'password' =&gt; array('name' =&gt; 'password'),
            ),
        );
    }

    public function login()
    {
        return array('username' =&gt; $this-&gt;username, 'password' =&gt; $this-&gt;password);
    }                                    
}</code></pre>
<p>当我们这样调用接口时：</p>
<pre><code>/?service=User.Login&amp;username=test&amp;password=123456</code></pre>
<p>就可以获取到需要的参数：</p>
<pre><code class="language-javascript">{"ret":0,"data":{"username":"test","password":"123456"},"msg":""}</code></pre>
<p>从中，可以很容易理解：参数规则需要统一配置在接口实现类里面的 <strong>getRules()</strong> 函数，随后即可以通过类成员属性方式获取，如： <strong>$this-&gt;username</strong> 。</p>
<h3>(2)更完善的示例</h3>
<p>很多时候我们都会对用户名和密码作一些验证，如是否必须、长度、最值，以及默认值等。  </p>
<p>继续上面的业务场景，我们登录下用户名和密码必须，且密码长度至少为6个字符，则可以调整参数规则：</p>
<pre><code class="language-javascript">'login' =&gt; array(
   'username' =&gt; array('name' =&gt; 'username', 'require' =&gt; true),
   'password' =&gt; array('name' =&gt; 'password', 'require' =&gt; true, 'min' =&gt; 6),
),</code></pre>
<p>尝试一下非法的参数请求，如无任何参数的情况下，访问/?service=User.Login，返回：</p>
<pre><code class="language-javascript">{"ret":400,"data":[],"msg":"Illegal Param: wrong param: username"}</code></pre>
<p>再尝试一下密码长不对的情况，访问/?service=User.Login&amp;username=test&amp;password=123，返回：</p>
<pre><code class="language-javascript">{"ret":400,"data":[],"msg":"Illegal Param: password.len should &gt;= 6, but now password.len = 3"}</code></pre>
<h2>1.12.4 三级参数</h2>
<h3>(1)系统参数</h3>
<p>已被系统固定占有的参数，目前只有一个，即：service，为需要调用的服务，类型为字符串，格式为：XXX.XXX，首字母不区分大小写，建议统一以大写开头。  </p>
<p>以下是一些示例：  </p>
<pre><code class="language-javascript">#推荐写法
/?service=User.GetBaseInfo

#正确写法（开头小写）
/?service=user.getBaseInfo
#正确写法（方法名小写，但类名只能开头小写，否则会导致linux系统下文件加载失败）
/?service=user.getbaseinfo

#错误写法（缺少方法名）
/?service=User
#错误写法（缺少点号分割）
/?service=UserGetBaseInfo
#错误写法（默认只支持点号分割）
/?service=User|GetBaseInfo</code></pre>
<h3>(2)应用参数</h3>
<p>应用参数是指在一个项目中，全部接口都需要的参数，或者通用的参数规则。假如我们的项目中全部需要签名sign参数，且必须；以及非必须的版本号，则可以在./Config/app.php中的apiCommonRules配置：</p>
<pre><code class="language-javascript">//$vim ./Config/app.php
&lt;?php

return array(
    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' =&gt; array(
        //签名
        'sign' =&gt; array(
            'name' =&gt; 'sign', 'require' =&gt; true,
        ),
        //客户端App版本号，如：1.0.1
        'version' =&gt; array(
            'name' =&gt; 'version', 'default' =&gt; '', 
        ),
    ),

    ... ...</code></pre>
<h3>(3)接口参数</h3>
<p>接口参数即为上面在各个接口子类中配置的规则，为特定接口所持有。同时，为了方便同一套接口的规则重用，可以使用下标为 '*' 表示是本接口通用规则，如我们为了加强安全性，为全部的用户接口操作都加上4位的验证码：</p>
<pre><code class="language-javascript">    public function getRules()
    {
        return array(
            '*' =&gt; array(
                'code' =&gt; array('name' =&gt; 'code', 'require' =&gt; true, 'min' =&gt; 4, 'max' =&gt; 4),
                ),
            'login' =&gt; array(
                'username' =&gt; array('name' =&gt; 'username', 'require' =&gt; true),
                'password' =&gt; array('name' =&gt; 'password', 'require' =&gt; true, 'min' =&gt; 6),
            ),
        );
    }</code></pre>
<p>在完成对上面的应用参数规则、接口通用规则和指定规则的参数进行配置后，对用户登录的接口进行请求时就需要这样访问：</p>
<pre><code class="language-javascript">/?service=User.login&amp;sign=77f81c17d512302383e5f26b99dae4d9&amp;username=test&amp;password=123456&amp;code=abcd</code></pre>
<blockquote>
<p>温馨提示：
在Api类里面配置规则时，下标不区分大小写。因为框架会自动将请求的函数名和全部的规则下标转换成小写进行匹配。</p>
</blockquote>
<p>这里，再小结一下，接口参数可以分为两种： <strong>通用接口参数</strong> 和 <strong>指定接口参数</strong> 。前者用 * 号下标表示，后者则用函数名作为下标表示。  </p>
<h3>(4)多个参数规则的优先级</h3>
<p>当同一个参数规则分别在应用参数、接口通用参数及特定接口参数出现时，后面的规则会覆盖前面的，即具体化的规则会替换通用的规则，以保证接口在特定场合的定制性。  </p>
<p>简而言之，多个参数规则的优先级从高到下，分别是（正如你想到的那样）：  </p>
<ul>
<li>1、指定接口参数</li>
<li>2、通用接口参数</li>
<li>3、应用参数</li>
<li>4、系统参数（通常忽略，因为只有service）</li>
</ul>
<h2>1.12.5 在线接口参数查询工具</h2>
<p>为了便于理解上面全部的参数规则，对于具体接口调用的要求，这里可以使用在线接口参数查询工具在浏览器访问查看：</p>
<pre><code class="language-javascript">/demo/checkApiParams.php?service=User.Login</code></pre>
<p>可以看到：<br />
<img src="http://webtools.qiniudn.com/20150411005257_9059c8a741f03da3b099f9042ced8678" alt="show" />  </p>
<p>此工具同时也可以方便客户端实时查看接口文档时，进行辅助的接口规则说明。</p>
<h3>自描述数据</h3>
<p>这里值得一提的是，我们这里所定义的参数规则实际上也是自描述数据。即配置的代码真实同步反映了参数的相关属性。  </p>
<h2>1.12.6 参数传递的方式</h2>
<p>系统下GET和POST皆可，但是推荐：</p>
<ul>
<li>
<p>1、service参数以GET方式传递，接口统一以/?service=XXX.XXX链接请求，便于交流，更重要的是当接口发生问题时，可以快速在服务器上通过nginx日记定位问题；</p>
</li>
<li>
<p>2、其他参数以POST方式传递，特别对于敏感数据，如密码，以相对保护数据安全；</p>
</li>
<li>
<p>3、在编写文档，或者进行调试时，可以全部临时使用GET方式，如本文档的写法，同时在浏览器时也可以使用GET；</p>
</li>
<li>4、如果需要对数据包进行加密或者压缩、自定义参数格式，可以重载PhalApi_Request::genData()，然后再继续使用参数规则解析；</li>
</ul>
<h2>1.12.7 参数规则</h2>
<table>
<thead>
<tr>
<th>类型type</th>
<th>参数名称 name</th>
<th>是否必须require</th>
<th>默认值default</th>
<th>最小值&amp;最大值min&amp;max</th>
<th>更多</th>
</tr>
</thead>
<tbody>
<tr>
<td>字符串</td>
<td>string</td>
<td>true/false，默认false</td>
<td>应为字符串</td>
<td>可选</td>
<td>regex下标为正则匹配的规则；format下标可用于定义字符编码的类型，如utf8、gbk,gb2312</td>
</tr>
<tr>
<td>整数</td>
<td>int</td>
<td>true/false，默认false</td>
<td>应为整数</td>
<td>可选</td>
<td>---</td>
</tr>
<tr>
<td>浮点数</td>
<td>float</td>
<td>true/false，默认false</td>
<td>应为浮点数</td>
<td>可选</td>
<td>---</td>
</tr>
<tr>
<td>布尔值</td>
<td>boolean</td>
<td>true/false，默认false</td>
<td>true/false</td>
<td>---</td>
<td>以下值会转换为true： ok, true, success, on, yes, 1</td>
</tr>
<tr>
<td>时间戳/日期</td>
<td>date</td>
<td>true/false，默认false</td>
<td>会按格式转换</td>
<td>可选，仅当为timestamp时才判断</td>
<td>格式：format 为timestamp时会将字符串的日期转换</td>
</tr>
<tr>
<td>数组</td>
<td>array</td>
<td>true/false，默认false</td>
<td>为非数组会自动转换/解析成数组</td>
<td>可选，判断数组元素个数</td>
<td>格式：format 为explode时，会根据separator将字符串分割成数组, 为json时，会json解析</td>
</tr>
<tr>
<td>枚举</td>
<td>enum</td>
<td>true/false，默认false</td>
<td>应为range中的某个元素</td>
<td>---</td>
<td>必须，range,以数组指定枚举的范围</td>
</tr>
<tr>
<td>文件</td>
<td>file</td>
<td>true/false，默认false</td>
<td>数组类型</td>
<td>min和max表示文件大小范围</td>
<td>range下标表示允许上传的文件类型，ext表示需要过滤的文件扩展名</td>
</tr>
<tr>
<td>回调</td>
<td>callable</td>
<td>true/false，默认false</td>
<td>---</td>
<td>---</td>
<td>callback设置回调函数，params为回调函数的第三个参数，第一个为参数值，第二个为所配置的规则</td>
</tr>
</tbody>
</table>
<blockquote>
<p>温馨提示：<br />
全部的参数规则，都可以配置desc下标，对应在线接口文档的”说明“部分。<br />
如： array('name' =&gt; 'username', 'desc' =&gt; '用户名')</p>
</blockquote>
<p>下面是对各类型的示例说明。</p>
<h3>(1)字符串 string</h3>
<p>当一个参数规则 未指定类型时，默认为string。一个完整的写法可以为：</p>
<pre><code class="language-javascript">array('name' =&gt; 'username', 'type' =&gt; 'string', 'require' =&gt; true, 'default' =&gt; 'nobody', 'min' =&gt; 1, 'max' =&gt; 10)</code></pre>
<p>若传递的参数长度过长，如&amp;username=alonglonglonglongname，则会异常失败返回：</p>
<pre><code class="language-javascript">{"ret":400,"data":[],"msg":"Illegal Param: username.len should &lt;= 10, but now username.len = 21"}</code></pre>
<p>但是当需要验证的是类型是中文的话会出现一点问题一个中文字符会占用3个字节所以在min和max验证的时候会出现一些问题,PhalApi提供了format方式对你需要验证长度的string进行指定格式可以排除此问题</p>
<pre><code class="language-javascript">array('name' =&gt; 'username', 'type' =&gt; 'string','format' =&gt; 'utf8', 'min' =&gt; 1, 'max' =&gt; 10)</code></pre>
<p>对于正则表达式的验证，一个邮箱的例子是：  </p>
<pre><code>'email' =&gt; array(
    'name' =&gt; 'email',
    'require' =&gt; true,
    'min' =&gt; '1',
    'regex' =&gt; "/^([0-9A-Za-z\\-_\\.]+)@([0-9a-z]+\\.[a-z]{2,3}(\\.[a-z]{2})?)$/i",
    'desc' =&gt; '邮箱',
),</code></pre>
<h3>(2)整型 int</h3>
<p>如通常数据库中的id，即可配置成：</p>
<pre><code class="language-javascript">array('name' =&gt; 'id', 'type' =&gt; 'int', 'require' =&gt; true, 'min' =&gt; 1 )</code></pre>
<p>当传递的参数，不在其配置的范围内时，如&amp;id=0，则会异常失败返回：</p>
<pre><code class="language-javascript">{"ret":400,"data":[],"msg":"Illegal Param: id should &gt;= 1, but now id = 0"}</code></pre>
<h3>(3)浮点 float</h3>
<p>浮点型，类似整型的配置，此处略。</p>
<h3>(4)布尔值 boolean</h3>
<p>布尔值，主要是可以对一些字符串转换成布尔值，如ok, true, success, on, yes, 以及会被PHP解析成true的字符串，都会转换成true，方便调用。如通常的是否记住我：</p>
<pre><code class="language-javascript">array('name' =&gt; 'isRememberMe', 'type' =&gt; 'boolean', 'default' =&gt; true)</code></pre>
<h3>(5)日期 date</h3>
<p>日期可以按自己约定的格式传递，可以这样配置：</p>
<pre><code class="language-javascript">array('name' =&gt; 'registerData', 'type' =&gt; 'date')</code></pre>
<p>对应地，risterData=2015-01-31 10:00:00则会被获取到为：&quot;2015-01-31 10:00:00&quot;。</p>
<p>当需要将字符串的日期转换成timestamp时，则配置成：</p>
<pre><code class="language-javascript">array('name' =&gt; 'registerData', 'type' =&gt; 'date', 'format' =&gt; 'timestamp')</code></pre>
<p>则上面的参数再请求时，则会被转换成：1422669600。</p>
<h3>(6)数组 array</h3>
<p>很多时候在接口进行批量获取时，都需要提供一组参数，所以这时可以使用数组来进行配置。如：</p>
<pre><code class="language-javascript">array('name' =&gt; 'uids', 'type' =&gt; 'array', 'format' =&gt; 'explode', 'separator' =&gt; ',')</code></pre>
<p>对应&amp;uids=1,2,3则会被转换成：</p>
<pre><code class="language-javascript">array ( 0 =&gt; '1', 1 =&gt; '2', 2 =&gt; '3', )</code></pre>
<p>又如接口需要使用JSON来传递整块参数时，可以这样配置：</p>
<pre><code class="language-javascript">array('name' =&gt; 'params', 'type' =&gt; 'array', 'format' =&gt; 'json')</code></pre>
<p>对应&amp;params={&quot;username&quot;:&quot;test&quot;,&quot;password&quot;:&quot;123456&quot;}则会被转换成：</p>
<pre><code class="language-javascript">array ( 'username' =&gt; 'test', 'password' =&gt; '123456', )</code></pre>
<p>特别地，当配置成了数组，却未指定格式format时，会转换成一个元素的数组，如：&amp;name=test，会转换成：array('test')。</p>
<h3>(7)枚举 enum</h3>
<p>在需要对接口参数进行范围限制时，可以使用此枚举型。如对于性别的参数，可以这样配置：</p>
<pre><code class="language-javascript">array('name' =&gt; 'sex', 'type' =&gt; 'enum', 'range' =&gt; array('female', 'male'))</code></pre>
<p>当传递的参数不合法时，如&amp;sex=unknow，则会被拦截，返回失败：</p>
<pre><code class="language-javascript">{"ret":400,"data":[],"msg":"Illegal Param: sex should be in female\/male, but now sex = unknow"}</code></pre>
<p>关于枚举类型的配置，这里需要特别注意配置时，应尽量使用字符串的值。<br />
因为通常而言，接口通过GET/POST方式获取到的参数都是字符串的，而如果配置规则时指定范围用了整型，会导致底层规则验证时误判。如：  </p>
<pre><code class="language-javascript">//接口参数为： &amp;type=N

//接口参数规则为：
array('name' =&gt; 'type', 'type' =&gt; 'enum', 'range' =&gt; array(0, 1, 2))

//误判，因为：
var_dump(in_array('N', array(0, 1, 2))); //结果为true，因为 'N' == 0</code></pre>
<p>为了避免这类情况发生，应该这样配置：  </p>
<pre><code class="language-javascript">//接口参数规则为（使用字符串）：
array('name' =&gt; '&amp;type', 'type' =&gt; 'enum', 'range' =&gt; array(`0`, `1`, `2`))</code></pre>
<h3>(8)文件 file</h3>
<p>在需要对上传的文件进行过滤、接收和处理时，可以使用文件类型，如：</p>
<pre><code class="language-javascript">array(
    'name' =&gt; 'upfile', 
    'type' =&gt; 'file', 
    'min' =&gt; 0, 
    'max' =&gt; 1024 * 1024, 
    'range' =&gt; array('image/jpeg', 'image/png') , 
    'ext' =&gt; array('jpeg', 'png')
)</code></pre>
<p>其中，min和max分别对应文件大小的范围，单位为字节；range为允许的文件类型，使用数组配置，且不区分大小写。 </p>
<p>如果成功，返回的值对应的是$_FILES[&quot;upfile&quot;]，即会返回：</p>
<pre><code class="language-javascript">array(
     'name' =&gt; '',
     'type' =&gt; '',
     'size' =&gt; '',
     'tmp_name' =&gt; '',
)</code></pre>
<p>对应的是：  </p>
<ul>
<li>$_FILES[&quot;upfile&quot;][&quot;name&quot;] - 被上传文件的名称</li>
<li>$_FILES[&quot;upfile&quot;][&quot;type&quot;] - 被上传文件的类型</li>
<li>$_FILES[&quot;upfile&quot;][&quot;size&quot;] - 被上传文件的大小，以字节计</li>
<li>$_FILES[&quot;upfile&quot;][&quot;tmp_name&quot;] - 存储在服务器的文件的临时副本的名称</li>
<li>$_FILES[&quot;upfile&quot;][&quot;error&quot;] - 由文件上传导致的错误代码</li>
</ul>
<p>若需要配置默认值default选项，则也应为一数组，且其格式应类似如上。</p>
<p>其中,ext是对文件后缀名进行验证,当如果上传文件后缀名不匹配时将抛出异常。文件扩展名的过滤可以类似这样进行配置：</p>
<pre><code>//单个后缀名 - 数组形式
'ext' =&gt; array('jpg')

//单个后缀名 - 字符串形式
'ext' =&gt; 'jpg'

//多个后缀名 - 数组形式
'ext' =&gt; array('jpg', 'jpeg', 'png', 'bmp')

//多个后缀名 - 字符串形式（以英文逗号分割）
'ext' =&gt; 'jpg,jpeg,png,bmp'</code></pre>
<h3>(9)回调 callable</h3>
<p>当需要利用已有函数进行自定义验证时，可采用回调参数规则，如：  </p>
<pre><code>//配置规则
array('name' =&gt; 'version', 'type' =&gt; 'callable', 'callback' =&gt; array('Common_MyVersion', 'formatVersion'))</code></pre>
<p>然后，回调时将调用下面这个函数：</p>
<pre><code>//新增一个自定义的版本检测函数
class Common_MyVersion {

    public static function formatVersion($value, $rule) {
        if (count(explode('.', $value)) &lt; 3) {
            throw new PhalApi_Exception_BadRequest('版本号格式错误');
        }
    }
}</code></pre>
<blockquote>
<p>温馨提示：
第一个为参数值，第二个为所配置的规则，第三个参数为配置规则中的params（可忽略）</p>
</blockquote>
<h2>1.12.8 关于参数设计的原则</h2>
<h3>(1)通配的$_REQUEST</h3>
<p>使用$_REQUEST获取参数，便于在不同场合下GET/POST之间的切换，同时在初始化DI()-&gt;request服务时，可以指定传递的参数，以便于灵活的单元测试；</p>
<h3>(2)更自由的名称映射</h3>
<p>之所以没把规则配置的下标默认成与客户端传递的name一致，是为了更自由的名称映射；<br />
如可能我们PHP后台开发喜欢用驼峰法来表示，但客户端想用下划线来分割，则通过这样配置：</p>
<pre><code class="language-javascript">array(
    'isRememberMe' =&gt; array('name' =&gt; 'is_remember_me', 'type' =&gt; 'boolean', 'default' =&gt; true),
)</code></pre>
<p>更重要的是，有时我们希望能缩短客户端请求的参数名称以节省流量时，可以这样配置：</p>
<pre><code class="language-javascript">array(
    'isRememberMe' =&gt; array('name' =&gt; 're', 'type' =&gt; 'boolean', 'default' =&gt; true),
)</code></pre>
<h3>(3)异常返回</h3>
<p>对于客户端参数不合法时，以异常失败返回，而不是隐性地转换，是因为后台接口往往需要手动对传递的参数进行人工的验证，而不是希望得到隐性转换的值。即当客户端参数传递不对时，我们需要明确提示说：参数非法。</p>
<h2>1.12.9 扩展你的参数</h2>
<p>当PhalApi提供的参数规则不能满足接口参数的规则验证时，除了使用callable类型进行扩展外，还可以扩展PhalApi_Request_Formatter接口来定制项目需要的类型。  </p>
<p>一如既往，分两步：  </p>
<ul>
<li>1、扩展实现PhalApi_Request_Formatter接口</li>
<li>2、在DI注册你的类型</li>
</ul>
<p>下面以大家所熟悉的邮件类型为例，说明扩展的步骤。  </p>
<p>首先，我们需要一个实现了邮件类型验证的功能类：  </p>
<pre><code>&lt;?php

class Common_MyFormatter_Email implements PhalApi_Request_Formatter {

    public function parse($value, $rule) {
        if (!preg_match('/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/', $value)) {
            throw new PhalApi_Exception_BadRequest('邮箱地址格式错误');
        }

        return $value;
    }
}  </code></pre>
<p>然后，注册一下：  </p>
<pre><code>DI()-&gt;_formatterEmail = 'Common_MyFormatter_Email';</code></pre>
<blockquote>
<p>温馨提示：
在DI中手动注册服务时，名称的格式为： 下划线(&quot;_&quot;) + 统一前缀（&quot;formatter&quot;) + 参数类型（全部小写后，首字母大写）；<br />
若需要实现自动注册，扩展的类名格式须为：  </p>
<pre><code>class PhalApi_Request_Formatter_{类型名称} implements PhalApi_Request_Formatter { ...</code></pre>
</blockquote>
<p>系统已自动注册的格式化服务有：  </p>
<ul>
<li>_formatterArray 数组格式化服务</li>
<li>_formatterBoolean 布尔值格式化服务</li>
<li>_formatterCallable 回调格式化服务</li>
<li>_formatterDate 日期格式化服务</li>
<li>_formatterEnum 枚举格式化服务</li>
<li>_formatterFile 上传文件格式化服务</li>
<li>_formatterFloat 浮点数格式化服务</li>
<li>_formatterInt 整数格式化服务</li>
<li>_formatterString 字符串格式化服务</li>
</ul>
<p>至此，便可使用自己定制的类型规则了，  </p>
<pre><code>array('name' =&gt; 'user_email', 'type' =&gt; 'email')</code></pre>
<hr />
<h4><a href="/wikis/%5B1.11%5D-%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8%28backup%29.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">下一章</a>#[1.13]-统一的接口请求方式：_sevice=XXX.XXX</h4>
<h4><a href="/wikis/%5B1.12%5D-%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">下一章</a></h4>
<hr />
<p><em>有句话说得好，最可怕的事情不是别人比你优秀，而是优秀的人竟然还比你更努力。  --《考拉小巫的留学成长日记》</em>  </p>
<p>此篇章主要是讲述接口统一请求的方式，以及提供一个PHP实现的简单客户端。</p>
<h2>1.13.1 指定接口服务：?service=XXX.XXX</h2>
<p>我们统一固定用service参数来表示需要请求获得的服务，并通过GET方式传递，即请求的URI格式为：</p>
<pre><code>接口域名 + 入口路径 + ?service=XXX.XXX

如：
http://dev.phalapi.com   +   /demo/  +  ?service=User.GetBaseInfo</code></pre>
<p>当我们在浏览器以GET方式请求时，可以在nignx看到这样的日记：</p>
<pre><code class="language-javascript">127.0.0.1 - - [07/Feb/2015:22:46:46 -0800] "GET /demo/?service=User.GetBaseInfo&amp;sign=&amp;user_id=1 HTTP/1.1" 200 107 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:11.0) Gecko/20100101 Firefox/11.0"</code></pre>
<p>如果通过接口用POST方式请求，则会看到：</p>
<pre><code class="language-javascript">127.0.0.1 - - [07/Feb/2015:19:32:05 -0800] "POST /demo/?service=User.GetBaseInfo&amp;sign= HTTP/1.1" 200 135 "-" "-"</code></pre>
<p>这里service的名称，开头不区分大小写，建议统一以大写开头，以显得专业。对应的接口是：</p>
<pre><code class="language-javascript">class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }
}</code></pre>
<h2>1.13.2 统一参数用GET</h2>
<p>在一个项目中，会有很多公共的接口参数，如客户端、版本号、密钥等。这些同样可以纳入GET参数里面，或者也可以放到POST里面。  </p>
<blockquote>
<p>温馨提示：<br />
这样要求是有目的的，因为这样的话可以在nginx的access日记里面查看来自客户端的快照信息，以便统计或者定位问题。  </p>
</blockquote>
<h2>1.13.3 接口参数用POST</h2>
<p>特别地，接口参数我们建议统一使用POST方式传递，理由很简单：  </p>
<ul>
<li>1、相对保护上传的数据，一如密码；</li>
<li>2、避免特殊字符或者过大数据包在GET下的限制；</li>
</ul>
<h2>1.13.4 获取Header头信息</h2>
<p>每个请求都有自己的Header头,获取Header头信息可以判断请求来源或者是把一些敏感的内容存放到Header头信息中(如:加密后的参数),框架也提供了方便快捷获取Header的方法:</p>
<pre><code class="language-javascript">DI()-&gt;request-&gt;getHeader("Host");</code></pre>
<h2>1.13.5 测试下的模拟参数</h2>
<p>默认地，PhalApi框架会将$_REQUEST作为接口参数的来源：</p>
<pre><code class="language-javascript">DI()-&gt;request = 'PhalApi_Request';</code></pre>
<p>当我们需要统一强制用$_GET，可以在init.php文件中这样简单定制：</p>
<pre><code class="language-javascript">DI()-&gt;request = new PhalApi_Request($_GET);</code></pre>
<p>同样，也可以在init.php文件中强制用$_POST：</p>
<pre><code class="language-javascript">DI()-&gt;request = new PhalApi_Request($_POST);</code></pre>
<p>在测试环境下，为了模拟接口请求，我们需要人工提供接口参数，因此可以这样轻松模拟：</p>
<pre><code class="language-javascript">$str = 'service=User.GetBaseInfo&amp;user_id=1';

parse_str($str, $params);
DI()-&gt;request = new PhalApi_Request($params);</code></pre>
<h2>1.13.6 PHP接口客户端示例</h2>
<p>先看下调用和使用的代码示例：</p>
<pre><code class="language-javascript">&lt;?php
require_once './PhalApiClient.php';

$config = array(
    'host' =&gt; 'http://dev.phalapi.com/demo',
    'secrect' =&gt; '******'
);

$client = new PhalApiClient($config);
$rs = $client-&gt;request('User.GetBaseInfo', array('userId' =&gt; 1));

if ($client-&gt;getRet() == PhalApiClient::RET_OK) {
    var_dump($rs);
} else {
    var_dump($client-&gt;getMsg());
    var_dump($client-&gt;getUrl());
}</code></pre>
<p>附调用接口的客户端源代码：</p>
<pre><code class="language-javascript">//$ vim ./PhalApiClient.php
&lt;?php

class PhalApiClient
{
    protected $host;
    protected $secrect = '';

    protected $params = array();

    protected $moreParams = array();

    protected $url;
    protected $ret;
    protected $msg;
    protected $data = array();

    const RET_OK = 'OK';
    const RET_WRONG = 'WRONG';
    const RET_ERROR = 'ERROR';

    public function __construct($config)
    {
        $this-&gt;host = rtrim($config['host'], '/') . '/';
        $this-&gt;secrect = $config['secrect'];
    }

    public function request($service, $params = array(), $timeoutMs = 3000)
    {
        if (!empty($service)) {
            $this-&gt;params['service'] = $service;
        }
        $this-&gt;params['sign'] = $this-&gt;encryptAppKey($params, $this-&gt;secrect);
        $this-&gt;url = $this-&gt;host . '?' . http_build_query($this-&gt;params);
        $this-&gt;moreParams = $params;

        $rs = $this-&gt;doRequest($this-&gt;url, $params, $timeoutMs);

        if ($rs === false) {
            $this-&gt;ret = self::RET_ERROR;
            $this-&gt;msg = '后台接口请求超时';
            return $this-&gt;getData();
        }

        $rs = json_decode($rs, true);

        if (isset($rs['data']['code']) &amp;&amp; $rs['data']['code'] != 0) {
            $this-&gt;ret = self::RET_WRONG;
            $this-&gt;msg = '接口调用失败[code =' . $rs['data']['code'] . ']' . ', 错误&gt;信息：' . isset($rs['data']['msg']) ? $rs['data']['msg'] : '无';
            return $this-&gt;getData();
        }

        $this-&gt;ret = intval($rs['ret']) == 200 ? self::RET_OK : self::RET_WRONG;
        $this-&gt;data = $rs['data'];
        $this-&gt;msg = $rs['msg'];

        return $this-&gt;getData();
    }

    public function getRet()
    {
        return $this-&gt;ret;
    }

    public function getData()
    {
        return $this-&gt;data;
    }

    public function getMsg()
    {
        return $this-&gt;msg;
    }

    public function getUrl()
    {
        return $this-&gt;url . '&amp;' . http_build_query($this-&gt;moreParams);
    }

    protected function encryptAppKey($params, $secrect)
    {
        return '';
    }

    protected function doRequest($url, $data, $timeoutMs = 3000)
    {
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeoutMs);

        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }

        $rs = curl_exec($ch);

        curl_close($ch);

        return $rs;
    }
}</code></pre>
<hr />
<h4><a href="/wikis/%5B1.12%5D-%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%8F%82%E6%95%B0%E8%A7%84%E5%88%99%E9%85%8D%E7%BD%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">下一章</a>#[1.14]-统一的返回格式和结构：ret-data-msg</h4>
<h4><a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.32%5D-%E5%9C%A8%E7%BA%BF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%EF%BC%9A%E6%B3%A8%E9%87%8A%E8%A7%84%E8%8C%83.html">下一章</a></h4>
<hr />
<p><em>表达，从简单开始。--《Robin Williams：写给大家看的设计书》</em>  </p>
<h2>1.14.1 统一返回的格式</h2>
<p>很明显地，默认情况下，我们选择了 <strong>JSON</strong> 作为统一的格式返回接口结果。这里简单说明一下选取JSON统一返回的原因：  </p>
<ul>
<li>JSON当前很流行，且普通接口都采用此格式返回</li>
<li>JSON在绝大部分开发语言中都支持，跨语言</li>
<li>
<p>JSON在浏览器浏览时，有可视化插件支持，如FF下：    </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_74d77c625f39db43cab421787f9674f4" alt="apic" /></p>
</li>
</ul>
<h2>1.14.2 统一返回结构</h2>
<p>通常，我们正常情况下请求接口会返回类似：  </p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423142802
    },
    "msg": ""
}</code></pre>
<p>其中，ret表示为返回状态码，200表示成功；data为领域业务数据，由接口自定义；最后msg为错误的提示信息。下面分别解释之。</p>
<h3>(1)返回状态码 ret</h3>
<p>参照HTTP的状态码，特约定：  </p>
<ul>
<li>200：接口正常请求并返回</li>
<li>4XX：客户端非法请求</li>
<li>5XX：服务器运行错误</li>
</ul>
<h4>200 正常返回</h4>
<p>当返回200时，需要同时返回data部分数据，以便客户端实现所需要的业务功能。  </p>
<h4>4XX 客户端非法请求</h4>
<p>此类请求是由客户端不正确调用引起的，如请求的接口服务不存在，或者接口参数不对，验证失败等等。当这种情况发生时，客户端同学只需要调整修正调用即可。  </p>
<p>对于此系统的状态码，在进行接口开发时，可由项目自已定义约定。  通常地，我们需要告知客户端签名失败时，可以这样：  </p>
<pre><code class="language-javascript">throw new PhalApi_Exception_BadRequest('wrong sign', 1);</code></pre>
<p>即抛出PhalApi_Exception_BadRequest异常即可，错误信息会返回客户端，对应msg字段；状态为1，系统对此类的异常会在400基础上相加的，即： <strong>401 = 400 + 1</strong> 。  </p>
<h4>5XX 服务器运行错误</h4>
<p>此类错误是应该避免的，但当客户端发现有这种情况时，应该知会后台接口开发人员进行修正。<br />
如当配置的参数规则不符合要求时，或者获取了不存在的参数等即会触发此类异常错误，通常由框架抛出。  </p>
<h3>(2)业务数据 data</h3>
<p>data为接口和客户端主要沟通对接的数据部分，可以为任何类型，由接口自定义。但为了更好地扩展、向后兼容，建议都使用array。  </p>
<h4>返回格式的定义与在线查看</h4>
<p>当我们在开发接口时，可以通过为接口添加注释的方式来定义接口的返回格式，然后就可以为外部提供在线文档的实时查看了。  </p>
<p>如：</p>
<pre><code class="language-javascript">&lt;?php

class Api_User extends PhalApi_Api {

    /**
     * 获取用户基本信息
     * @desc 用于获取单个用户基本信息
     * @return int code 操作码，0表示成功，1表示用户不存在
     * @return object info 用户信息对象
     * @return int info.id 用户ID
     * @return string info.name 用户名字
     * @return string info.note 用户来源
     * @return string msg 提示信息
     */
    public function getBaseInfo() {
        // ... ...
    }
</code></pre>
<p>然后在浏览器访问：<br />
<a href="http://demo.phalapi.net/checkApiParams.php?service=User.getBaseInfo">http://demo.phalapi.net/checkApiParams.php?service=User.getBaseInfo</a>  </p>
<p>可以看到：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/20170205152848.jpg" alt="apic" />  </p>
<h4>注释格式</h4>
<p>格式是以docs的 return 注释来标明的，其格式为：</p>
<pre><code>@return  返回的类型  字段名字路径(以点号连接)  字段名字及解析</code></pre>
<p>其中，返回的类型可以为：  </p>
<table>
<thead>
<tr>
<th>关键字</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>string</td>
<td>字符串</td>
</tr>
<tr>
<td>int</td>
<td>整型</td>
</tr>
<tr>
<td>float</td>
<td>浮点型</td>
</tr>
<tr>
<td>boolean</td>
<td>布尔型</td>
</tr>
<tr>
<td>date</td>
<td>日期</td>
</tr>
<tr>
<td>array</td>
<td>数组</td>
</tr>
<tr>
<td>fixed</td>
<td>固定值</td>
</tr>
<tr>
<td>enum</td>
<td>枚举类型</td>
</tr>
<tr>
<td>object</td>
<td>对象</td>
</tr>
</tbody>
</table>
<blockquote>
<p>温馨提示：array与object的区别<br />
array是指没有下标的一个数组集合，或者有下标但下标是连续的自然数，且各元素的结构相同；object则是指一个结构体，类似字典。  </p>
</blockquote>
<p>此外，为了明确数组与对象间的返回格式，我们也推荐如果是元素来自数组，则在返回字段的后面添加方括号来表明，以提醒客户端在接收到此类返回时需要循环处理。如：</p>
<pre><code class="language-javascript">     * @return array list 用户列表
     * @return int list[].id 用户ID
     * @return string list[].name 用户名字
     * @return string list[].note 用户来源
</code></pre>
<p>当需要对接口进行更多说明时，可使用@desc注释，即：</p>
<pre><code>     * @desc 用于获取单个用户基本信息</code></pre>
<h3>(3)错误信息 msg</h3>
<p>当返回状态码不为200时，此字段不为空。即当有异常（如上面所说的客户端非法请求和服务端运行错误两大类）触发时，会自动将异常的错误信息作为错误信息msg返回。  </p>
<p>但对于服务端的异常，出于对接口隐私的保护，框架在错误信息时没有过于具体地描述；相反，对于客户端的异常，由会进行必要的说明，以提醒客户端该如何进行调用调整。  </p>
<p>此外，我们根据需要可以考虑是否需要进行国际化的翻译。如果项目在可预见的范围内需要部署到国外时，提前做好翻译的准备是很有帮助的。如下，开发时可以这样返回异常错误信息：</p>
<pre><code class="language-javascript">throw new PhalApi_Exception_BadRequest(T('wrong sign'), 1);</code></pre>
<h3>(4)头部信息 header</h3>
<p>当使用的是HTTP/HTTPS协议时，并且需要设置头部header时，可以使用<code>PhalApi_Response::addHeaders()</code>进行设置。对于JSON格式的返回默认设置的头部有：  </p>
<pre><code>Content-Type:"application/json;charset=utf-8"</code></pre>
<p>若需要其他的头部设置，可类似这样，在<code>init.php</code>初始化文件进行设置跨域访问（但通常不建议允许全部跨域，这里仅作演示）：  </p>
<pre><code>DI()-&gt;response-&gt;addHeaders('Access-Control-Allow-Origin', '*');</code></pre>
<p>后面相同的头部信息会覆盖前面的。  </p>
<h2>1.14.3 关于Exception类异常没捕捉的原因</h2>
<p>我们没有对Exception类的异常进行捕捉，封装返回非200的形式，是因为我们出于以下的考虑：</p>
<ul>
<li>一来为了方便开发过程中快速发现及定位具体出错的位置；</li>
<li>二来为了便于线上环境中nginx服务器对错误的捕捉和纪录；</li>
</ul>
<h2>1.14.4 JsonP格式和其他的返回</h2>
<p>在部分H5页面异步请求的情况下，客户端需要我们返回JSONP格式的结果，则可以这样在入口文件重新注册response：</p>
<pre><code class="language-javascript">if (!empty($_GET['callback'])) {
    DI()-&gt;response = new PhalApi_Response_JsonP($_GET['callback']);
}</code></pre>
<p>但是在测试环境中，我们是不希望有内容输出的，所以我们可以测试时这样注册response：</p>
<pre><code class="language-javascript">DI()-&gt;response = 'PhalApi_Response_Explorer';</code></pre>
<h2>1.14.5 扩展你的返回格式</h2>
<p>当你的项目需要返回其他格式时，如返回XML，则可以先这样实现你的格式类：</p>
<pre><code class="language-javascript">class MyResponse_XML extends PhalApi_Response {

    protected function formatResult($result) {
        //TODO：把数组$result格式化成XML ...
    }
}</code></pre>
<p>随后，也是简单重新注册一下即可：</p>
<pre><code class="language-javascript">DI()-&gt;response = new MyResponse_XML();</code></pre>
<h2>1.14.6 各状态码产生的时机</h2>
<p><img src="http://webtools.qiniudn.com/20150413210910.jpg" alt="各状态码产生的时机" />  </p>
<h2>1.14.7 更好地建议</h2>
<p>很多时候，很多业务场景，客户端在完成一个接口请求并获取到所需要的数据后，需要进行不同的处理的。 </p>
<ul>
<li>就登录来说，当登录失败时，可能需要知道：</li>
<li>是否用户名不存在？</li>
<li>是否密码错误？</li>
<li>是否已被系统屏蔽？</li>
<li>是否密码错误次数超过了最大的重试次数？</li>
<li>...</li>
</ul>
<p>显然，这里也有一个返回状态码，更准备来说，是业务操作状态码。并且，此类的状态依接口不同而不同，很难做到统一。  </p>
<p>SO？  </p>
<p>我们建议的是，项目接口在业务数据data里面统一再定义一个状态码，通常为code字段，完整路径即： <strong>data.code</strong> ，同时为0时表示操作成功，非0时为不同的失败场景。如上面的登录：</p>
<ul>
<li>code = 0 登录成功</li>
<li>code = 1 用户名不存在</li>
<li>code = 2 密码错误</li>
<li>code = 3 系统已屏蔽此账号</li>
<li>code = 4 密码错误次数超过了最大的重试次数</li>
<li>...</li>
</ul>
<p>最后，客户端在获取到接口返回的数据后，先统一判断ret是否正常请求并正常返回，即ret = 200；若是，则再各自判断操作状态码code是否为0，如果不为0，则提示相应的文案并进行相应的引导，如果为0，则走正常流程！</p>
<h2>1.14.8 领域特定设计与Fiat标准</h2>
<p>在《RESTful Web APIs》一书中提及到，标准可以划归到4个分类，分别是：fiat标准、个人标准、公司标准以及开放标准。  </p>
<p>显然，我们这里推荐的是 <strong>JSON + ret-data-msg</strong> 返回格式既不是个人标准，也不是公司标准（就笔者观察的范筹而言，未发现某个公司定义了此格式）。而且，也不属于开放标准，因为也还没达到此程度。更多的，它是fiat标准。<br />
我们很容易发现，身边的应用、系统以及周围项目都在使用诸如此类的返回结构格式，如一些AJAX的接口。  </p>
<p>当然，我们可希望可以消除语义上的鸿沟，以便在后台接口开发上有一个很好地共识。  </p>
<p>同时，<strong>JSON + ret-data-msg</strong> 返回格式也是一种领域特定的格式，它更多是为app多端获取业务数据而制作的规范。虽然它不是很完美，不具备自描述消息，也没有资源链接的能力，但我们认为它是一种恰到好处的格式。<br />
在基于JSON通用格式的基础上，加以 <strong>ret-data-msg</strong> 的约束，它很好地具备了统一性，可能门槛低，容易理解。</p>
<hr />
<h4><a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.32%5D-%E5%9C%A8%E7%BA%BF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%EF%BC%9A%E6%B3%A8%E9%87%8A%E8%A7%84%E8%8C%83.html">下一章</a>#[1.15]-数据库操作：基于NotORM的使用及优化</h4>
<h4><a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.16%5D-%E9%85%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%EF%BC%9A%E5%86%85%E5%A4%96%E7%BD%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E7%9A%84%E5%AE%8C%E7%BE%8E%E5%88%87%E6%8D%A2.html">下一章</a></h4>
<hr />
<p><em>有些裁缝只会塞塞衬里，修修补补，而有些裁缝却能做出新东西--他们之间隔着一条深渊。 -- 《外套》</em>  </p>
<h2>1.15.1 NotORM官网</h2>
<p>这里使用了NotORM进行DB操作，具体的数据库操作使用文档请见NotORM官网： <a href="http://www.notorm.com"><a href="http://www.notorm.com">http://www.notorm.com</a></a>  </p>
<h3>(1) 基本CURD</h3>
<pre><code class="language-javascript">//查询
$row = DI()-&gt;notorm-&gt;user-&gt;where('id', 1)-&gt;fetch();

//更新
$data = array('name' =&gt; 'test', 'update_time' =&gt; time());
DI()-&gt;notorm-&gt;user-&gt;where('id', 1)-&gt;update($data);

//插入（须是同一个对象才能正确获取插入的ID）
$data = array('name' =&gt; 'phalapi');
$userORM = DI()-&gt;notorm-&gt;user;
$userORM-&gt;insert($data);
$id = $userORM-&gt;insert_id();

//删除
DI()-&gt;notorm-&gt;user-&gt;where('id', 1)-&gt;delete();</code></pre>
<h3>(2)update相同的数据的判断</h3>
<p>在使用update操作时，如果更新的数据和原来的一样，则会返回0（影响0行）。这时，会和更新失败（同样影响0行）混淆。  </p>
<p>但NotORM是一个优秀的类库，所以提供了优秀的解决文案。我们在使用update时，只须了解这两者返回的结果的微妙区别即可。<br />
因为失败异常时，返回false；而相同数据更新会返回0。即：  </p>
<ul>
<li>1、update相同的数据时，返回0，严格来说是：int(0)</li>
<li>2、update失败时，如更新一个不存在的字段，返回false，即：bool(false)</li>
</ul>
<p>用代码表示，就是：</p>
<pre><code class="language-javascript">$rs = DI()-&gt;notorm-&gt;user-&gt;where('id', $userId)-&gt;update($data);

if ($rs &gt;= 1) {
    //成功
} else if ($rs === 0) {
    //相同数据，无更新
} else if ($rs === false) {
    //更新失败
}</code></pre>
<p>以下单元测试代码，可以验证上面的判断：</p>
<pre><code class="language-javascript">    public function testUpdateOk()
    {
        $userId = 87;

        $rs = DI()-&gt;notorm-&gt;user-&gt;where('id', $userId)-&gt;update(array('reg_time' =&gt; time()));

        $this-&gt;assertSame(1, $rs);
    }

    public function testUpdateZero()
    {
        $userId = 1;

        $rs = DI()-&gt;notorm-&gt;user-&gt;where('id', $userId)-&gt;update(array('username' =&gt; 'aevit'));

        $this-&gt;assertSame(0, $rs);
    }

    public function testUpdateFail()
    {
        $userId = 1;

        $rs = DI()-&gt;notorm-&gt;user-&gt;where('id', $userId)-&gt;update(array('wrong_username' =&gt; 'aevit'));

        $this-&gt;assertSame(FALSE, $rs);
    }</code></pre>
<h3>(3)简单的关联查询</h3>
<p>如果是简单的关联查询，可以使用NotORM支持的写法，这样的好处在于我们使用了一致的开发，并且能让PhalApi框架保持分布式的操作方式（注意，关联的表仍然需要在同一个数据库）。  </p>
<p>以下是一个简单的示例。  </p>
<p>假设我们有这样的数据：</p>
<pre><code class="language-javascript">INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');
INSERT INTO `phalapi_user_session_0` VALUES ('1', '1', 'ABC', '', '0', '0', '0', null);</code></pre>
<p>那么对应关联查询的代码如下面：</p>
<pre><code class="language-javascript">    public function testLeftJoin()
    {
        $rs = DI()-&gt;notorm-&gt;user_session_0
            -&gt;select('expires_time, user.username, user.nickname')
            -&gt;where('token', 'ABC')
            -&gt;fetchRow();

        var_dump($rs);
    }
</code></pre>
<p>运行一下，我们可以看到这样的输出：</p>
<pre><code class="language-javascript">SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;

.[1 - 0.06318s]SELECT expires_time, user.username, user.nickname FROM phalapi_user_session_0 LEFT JOIN phalapi_user AS user ON phalapi_user_session_0.user_id = user.id WHERE (token = 'ABC') LIMIT 1;&lt;br&gt;
array(3) {
  ["expires_time"]=&gt;
  string(1) "0"
  ["username"]=&gt;
  string(35) "wx_edebc877070133c65161d00799e00544"
  ["nickname"]=&gt;
  string(10) "weixinName"
}</code></pre>
<p>这样，我们就可以实现关联查询的操作。按照NotORM官网的说法，则是：</p>
<blockquote>
<p>If the dot notation is used for a column anywhere in the query (&quot;$table.$column&quot;) then NotORM automatically creates left join to the referenced table. Even references across several tables are possible (&quot;$table1.$table2.$column&quot;). Referencing tables can be accessed by colon: $applications-&gt;select(&quot;COUNT(application_tag:tag_id)&quot;).</p>
</blockquote>
<p>-&gt;select('expires_time, user.username, user.nickname')这一行调用将会【自动产生关联操作】，而ON 的字段，则是这个字段关联你配置的【表结构】，外键默认为： 表名_id 。</p>
<h3>(4)加1操作</h3>
<p>NotORM已提供了NotORM_Literal，其用法如下：  </p>
<pre><code class="language-javascript">DI()-&gt;notorm-&gt;user-&gt;where('id', 1)-&gt;update(array('age' =&gt; new NotORM_Literal("age + 1")));</code></pre>
<p>当需要更新为当前时间，可以：  </p>
<pre><code class="language-javascript">$array = array(
    "title" =&gt; "NotORM",
    "author_id" =&gt; null,
    "created" =&gt; new NotORM_Literal("NOW()"),
);</code></pre>
<h2>1.15.2 NotORM的优化</h2>
<p>但为了更符合项目的开发，这里对NotORM的底层作了升级修改，以下为主要修改点和新的使用：  </p>
<h3>(1)将原来返回的结果全部从对象改成数组</h3>
<p>对原来的大部分使用无特别影响，可按原来的方式开发。主要目的是为了更方面处理返回的数据，以及简化对结果的再解析，简单明了。<br />
如：</p>
<pre><code class="language-javascript">DI()-&gt;notorm-&gt;user-&gt;where('username = ?', 'dogstar')-&gt;fetch();</code></pre>
<p>返回的将是一个数组：</p>
<pre><code class="language-javascript">array(7) {
  ["id"]=&gt;
  string(3) "180"
  ["username"]=&gt;
  string(17) "dogstar"
  ["regtime"]=&gt;
  string(10) "1414811954"
  //...
}</code></pre>
<h3>(2)提供获取全部结果的接口 - fetchAll() / fetchRows()</h3>
<p>如：  </p>
<pre><code class="language-javascript">$rows = DI()-&gt;notorm-&gt;event_picurl-&gt;where('eid', $eids)-&gt;fetchAll();</code></pre>
<p>或：</p>
<pre><code class="language-javascript">$rows = DI()-&gt;notorm-&gt;event_picurl-&gt;where('eid', $eids)-&gt;fetchRows();</code></pre>
<p>即可获取全部的数据，不再受限于分页。<br />
这里提供了fetchAll()和fetchRows()两种等效的操作，是为了减少记忆的痛苦，下同。  </p>
<h3>(3)提供更灵活的查询方式 - queryAll() / queryRows()</h3>
<p>当需要进行复杂的SQL查询时，可以使用此接口，如：<br />
(注意:limit替换值:start和:num必须使用int类型)</p>
<pre><code class="language-javascript">$sql = 'select * from example AS ep LEFT JOIN user AS u ON ep.ui
d = u.id  where ep.touid = :userId ORDER BY dateline desc LIMIT :start,:num';
$params = array(':userId' =&gt; $userId, ':start' =&gt; $start, ':num' =&gt; $num);
$rs= DI()-&gt;notorm-&gt;example-&gt;queryAll($sql, $params);</code></pre>
<p>或：</p>
<pre><code class="language-javascript">$rs= DI()-&gt;notorm-&gt;example-&gt;queryRows($sql, $params);</code></pre>
<h3>(4)limit 操作的调整</h3>
<p>取消了NotORM中对OFFSET关键字的使用，改用逗号的写法，修改后正确的使用方法应该是：  </p>
<pre><code class="language-javascript">$table-&gt;limit(10);  // limit 10   # 查询前10个

$table-&gt;limit(5, 10); // limit 5,10   # 从第5个位置开始，查询前10个</code></pre>
<h3>(5)禁止全表删除，防止误删</h3>
<p>出于对数据的保护，当执行删除操作却又没有任何where条件时，将会禁止进行全表操作。如：  </p>
<pre><code class="language-javascript">    public function testDeleteAll()
    {
        DI()-&gt;notorm-&gt;user-&gt;delete();
    }</code></pre>
<p>可以看到：  </p>
<pre><code class="language-javascript">$ phpunit --filter testDeleteAll ./Api/Api_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

E

Time: 315 ms, Memory: 6.25Mb

There was 1 error:

1) PhpUnderControl_ApiUser_Test::testDeleteAll
Exception: sorry, you can not delete the whole table --dogstar
</code></pre>
<h3>(6)添加&amp; __sql__ =1请求参数，可开启HTTP调试模式</h3>
<p>当处于debug模式时，可以输入执行的全部SQL语句，以便调试。   </p>
<p>如：  </p>
<pre><code class="language-javascript">SELECT times FROM tpl_user_session_10 WHERE (user_id = ?); -- '74110'
{"ret":0,"data":{"code":0},"msg":""}</code></pre>
<h3>(7)关于NotORM中fetch()操作没有limit 1的处理方案 - fetchOne() / fetchRow()</h3>
<p>之前，有开发同学提及到，为什么notorm的基类fetch为啥没用limit(1)呢。后来，我去发了下NotORM的写法，确实做得很微妙。<br />
其实NotORM之所以没有在fetch()里面自动limit 1是因为，你可以循环地获取数据，如：</p>
<pre><code class="language-javascript">$user = DI()-&gt;notorm-&gt;user-&gt;select('id, username, nickname')-&gt;where('id &gt; ?', 0)-&gt;limit(3);
while(($row = $user-&gt;fetch())) {
    var_dump($row);
 }</code></pre>
<p>但是，更多情况下，我们只需要获取某一行的数据，上面的做法会造成不必要的SQL查询。为了保留原来的写法，我特意添加扩展了一个新的操作：fetchRow()，用法同fetch()，但只会取第一条。<br />
以下是使用示例：  </p>
<pre><code class="language-javascript">$rs = DI()-&gt;notorm-&gt;user-&gt;select('id, username, nickname')-&gt;where('id &gt; ?', 0)-&gt;fetchRow());

var_dump($rs);

//结果输出类如：
array(3) {
  ["id"]=&gt;
  string(1) "1"
  ["username"]=&gt;
  string(5) "aevit"
  ["nickname"]=&gt;
  string(4) "test"
}

//对应执行的SQL语句：
[2 - 0.06544s]SELECT id, username, nickname FROM fami_user WHERE (id &gt; ?) LIMIT 1; -- 0&lt;br&gt;</code></pre>
<p>如果，我们只需要获取这一行的某个字段，也可以像fetch()那样使用，即：</p>
<pre><code class="language-javascript">$rs = DI()-&gt;notorm-&gt;user-&gt;select('id, username, nickname')-&gt;where('id &gt; ?', 0)-&gt;fetchRow('nickname'));

var_dump($rs);

//结果输出类如：
string(4) "test"

//纪录不存在时，返回 bool(false)</code></pre>
<h3>(8)显式的SQL语法异常提示</h3>
<p>很多时候，在开发时，我们对数据库操作一开始会存在一些SQL语法的问题，PDO会返回false,且原来NotORM也是使用 <strong>静默方式</strong> 来处理这类错误，从而使得开发人员有时难以发现这些问题，除非将调试的SQL手动放到数据库执行才能发现问题所在。  </p>
<p>为了能给开发同学更早、更直观的方式查看问题的所在，这里对NotORM底层进行了调整，使用了 <strong>显式方式</strong> 的策略来处理，即：直接抛出PDO异常。  </p>
<p>如：</p>
<pre><code class="language-javascript">$userId = 1;

//OK
$rs = DI()-&gt;notorm-&gt;user-&gt;select('username')-&gt;where('id', $userId)-&gt;fetchOne();

//WRONG
$rs = DI()-&gt;notorm-&gt;user-&gt;select('wrong_username')-&gt;where('id', $userId)-&gt;fetchOne();
</code></pre>
<p>将会看到：</p>
<pre><code class="language-javascript">[1 - 0.06437s]SELECT username FROM fami_user WHERE (id = 1) LIMIT 1;&lt;br&gt;
[2 - 0.06496s]SELECT wrong_username FROM fami_user WHERE (id = 1) LIMIT 1;&lt;br&gt;

PDOException: Unknown column 'wrong_username' in 'field list'</code></pre>
<h3>(9)复杂的关联查询</h3>
<p>如果是复杂的关联查询，则是建议使用原生态的SQL语句，但我们仍然可以保持很好的写法，如这样一个示例：</p>
<pre><code class="language-javascript">        $sql = 'SELECT t.id, t.team_name, v.vote_num '
            . 'FROM phalapi_team AS t LEFT JOIN phalapi_vote AS v '
            . 'ON t.id = v.team_id '
            . 'ORDER BY v.vote_num DESC';
        $rows = $this-&gt;getORM()-&gt;queryAll($sql, array());
</code></pre>
<p>注意，此时的表需要使用全名，即自带前缀。这样也可以实现更自由的关联查询。   </p>
<h3>(10)事务操作</h3>
<p>关于事务操作，可以参考 <a href="http://www.notorm.com/#api">NotORM官网</a> 的说明：  </p>
<pre><code>$db-&gt;transaction = $command Assign 'BEGIN', 'COMMIT' or 'ROLLBACK' to start or stop transaction </code></pre>
<p>即：  </p>
<pre><code>//第一步：先指定待进行事务的数据库（通过获取一个notorm表实例来指定；否则会提示：PDO There is no active transaction）
$user = DI()-&gt;notorm-&gt;user;

//第二步：开启事务开关（此开关会将当前全部打开的数据库都进行此设置）
DI()-&gt;notorm-&gt;transaction = 'BEGIN';

//第三步：进行数据库操作
$user-&gt;insert(array('name' =&gt; 'test1',));
$user-&gt;insert(array('name' =&gt; 'test2',));

//第四：提交/回滚
DI()-&gt;notorm-&gt;transaction = 'COMMIT';
//DI()-&gt;notorm-&gt;transaction = 'ROLLBACK';</code></pre>
<h4>推荐使用PhalApi的事务操作方式</h4>
<p>PhalApi一开始对事务这块考虑不周，后来发现很多同学、很多项目都需要用到数据库事务操作。<br />
基于此，在不破坏原来的代码基础上，我们决定在PhalApi_DB_NotORM上添加对数据库维度的事务操作支持。  </p>
<p>示例简单如下：  </p>
<pre><code>    public function testTransactionCommit()
    {
        //Step 1: 开启事务
        DI()-&gt;notorm-&gt;beginTransaction('db_demo');

        //Step 2: 数据库操作
        DI()-&gt;notorm-&gt;user&gt;insert(array('name' =&gt; 'test1'));
        DI()-&gt;notorm-&gt;user&gt;insert(array('name' =&gt; 'test2'));

        //Step 3: 提交事务
        DI()-&gt;notorm-&gt;commit('db_demo');

    }</code></pre>
<blockquote>
<p>温馨提示： 以上操作，须PhalApi 1.3.1 及以上版本才能支持。  </p>
</blockquote>
<h3>(11)扩展对非MySQL数据库的支持</h3>
<p>PhalApi使用的是NotORM来进行数据库操作，而NotORM底层则是采用了PDO。目前，NotORM支持： MySQL, SQLite, PostgreSQL, MS SQL, Oracle (Dibi support is obsolete)。  </p>
<p>但需要注意的是，PhalApi本身对NotORM进行了修改，需要调整一下代码才能更好地支持除MySQL外的数据库。
即使NotORM不支持的数据库，你也可以轻松通过添加扩展的方式来支持。如：   </p>
<p>首先，定制自己的数据库连接的PDO。  </p>
<pre><code>class Common_MyDB extends PhalApi_DB_NotORM {

    protected function createPDOBy($dbCfg) {
        /* Connect to an ODBC database using driver invocation */
    $dsn = 'uri:file:///usr/local/dbconnect';
    return new PDO($dsn, $dbCfg['user'], $dbCfg['password']);
    }
}</code></pre>
<p>随后，在初始化文件init.php中重新注册DI()-&gt;notorm即可，如：</p>
<pre><code>//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()-&gt;notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new Common_MyDB(DI()-&gt;config-&gt;get('dbs'), $debug);
};</code></pre>
<h2>1.15.3 可选的Model基类</h2>
<h3>(1)表数据入口模式</h3>
<p>我们一直在考虑，是否应该提供数据库的基本操作支持，以减少开发人员重复手工编写基本的数据操作。  </p>
<p>最后，我们认为是需要的。然后就引发了新的问题：是以继承还是以委托来支持？  </p>
<p>委托有助于降低继承的层级，但仍然需要编写同类的操作然后再次委托。所以，这里提供了基于NotORM的Model基类：PhalApi_Model_NotORM。  </p>
<p>然而提供这个基类还是会遇到一些问题，例如：如何界定基本操作？如何处理分表存储？如何支持定制化？  </p>
<p>由于我们这里的Model使用了 <strong>“表数据入口”</strong> 模式，而不是“行数据入口”，也不是“活动纪录”，也不是复杂的“数据映射器”。所以在使用时可以考虑是否需要此基类。即使这样，你也可以很轻松转换到“行数据入口”和“活动纪录”模式。这里，PhalApi中的Model是更广义上的数据源层（后面会有更多说明），因此对应地PhalApi_Model_NotORM基类充当了数据库表访问入口的对象，处理表中所有的行。  </p>
<h3>(2)规约层的CURD</h3>
<p>在明白了Model基类的背景后，再来了解其具体的操作和如何继承会更有意义。  </p>
<p>而具体的操作则与数据表的结构相关，在“约定编程”下：即每一个表都有一个主键（通常为id，也可以自由配置）以及一个序列化LOB字段ext_data。我们很容易想到Model接口的定义（注释已移除，感兴趣的同学可查看源码）：</p>
<pre><code class="language-javascript">interface PhalApi_Model {

    public function get($id, $fields = '*');

    public function insert($data, $id = NULL);

    public function update($id, $data);

    public function delete($id);
}
</code></pre>
<p>上面的接口在规约层上提供了基于表主键的CURD基本操作，在具体实现时，需要注意两点：一是分表的处理；另一点则是LOB字段的序列化。  </p>
<h3>(3)不使用Model基类的写法</h3>
<p>由于我们使用了NotORM进行数据库的操作，所以这里也提供了基于NotORM的基类：PhalApi_Model_NotORM。下面以我们熟悉的获取用户的基本信息为例，说明此基类的使用。</p>
<p>为唤醒记忆，下面贴上Model_User类原来的代码：</p>
<pre><code class="language-javascript">// $ vim ./Demo/Model/User.php

&lt;?php

class Model_User {

    public function getByUserId($userId) {
        return DI()-&gt;notorm-&gt;user-&gt;select('*')-&gt;where('id = ?', $userId)-&gt;fetch();
    }
}</code></pre>
<p>对应的调用：</p>
<pre><code class="language-javascript">$model = new Model_User();
$rs = $model-&gt;getByUserId($userId);</code></pre>
<h3>(4)继承Model基类的写法</h3>
<p>若继承于PhalApi_Model_NotORM，则是：</p>
<pre><code class="language-javascript">// $ vim ./Demo/Model/User.php

&lt;?php

class Model_User extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        return 'user';
    }
}</code></pre>
<p>从上面的代码可以看出，基类已经提供了基于主键的CURD操作，但我们需要钩子函数以返回对应的表名。相应地，外部调用则调整为：</p>
<pre><code class="language-javascript">$model = new Model_User();
$rs = $model-&gt;get($userId);</code></pre>
<p>再进一步，我们可以得到其他的基本操作：</p>
<pre><code class="language-javascript">$model = new Model_User();

//查询
$row = $model-&gt;get(1);
$row = $model-&gt;get(1, 'id, name'); //取指定的字段
$row = $model-&gt;get(1, array('id', 'name')); //可以数组取指定要获取的字段

//更新
$data = array('name' =&gt; 'test', 'update_time' =&gt; time());
$model-&gt;update(1, $data); //基于主键的快速更新

//插入
$data = array('name' =&gt; 'phalapi');
$id = $model-&gt;insert($data);
//$id = $model-&gt;insert($data, 5); //如果是分表，可以这样指定

//删除
$model-&gt;delete(1);</code></pre>
<h2>1.15.4 定制化你的Model基类</h2>
<p>正如上面提及到的两个问题：LOB序列化和分表处理。所以，如果PhalApi现有就此两问题的解决方案不能满足项目的需求，可作定制化处理。  </p>
<h3>(1)LOB序列化</h3>
<p>先是LOB序列化，考虑到有分表的存在，当发生数据库变更时（特别在线上环境）会有一定的难度和风险，因此引入了扩展字段ext_data。当然，此字段也应对数据库变更的同时，也可以作为简单明了的值对象的大对象。序列化LOB首先要考虑的问题是使用二进制（BLOB）还是文本（CLOB），出于通用性、易读性和测试性，我们目前使用了json格式的文本序列化。所以，如果考虑到空间或性能问题（在少量数据下我认为问题不大，如果数据量大，应该及时重新调整数据库表结构），可以重写formatExtData() &amp; parseExtData()。  </p>
<p>如改成serialize序列化：</p>
<pre><code class="language-javascript">abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    /**
     * 对LOB的ext_data字段进行格式化(序列化)
     */
    protected function formatExtData(&amp;$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = serialize($data['ext_data']);
        }
    }

    /**
     * 对LOB的ext_data字段进行解析(反序列化)
     */
    protected function parseExtData(&amp;$data) {
        if (isset($data['ext_data'])) {
            $data['ext_data'] = unserialize($data['ext_data'], true);
        }
    }

    // ...
}</code></pre>
<p>将Model类继承于Common_Model_NotORM后，  </p>
<pre><code class="language-javascript">// $ vim ./Demo/Model/User.php

&lt;?php
class Model_User extends Common_Model_NotORM {
   //...
}</code></pre>
<p>就可以轻松切换到序列化，如：  </p>
<pre><code class="language-javascript">$model = new Model_User();

//带有ext_data的更新
$extData = array('level' =&gt; 3, 'coins' =&gt; 256);
$data = array('name' =&gt; 'test', 'update_time' =&gt; time(), 'ext_data' =&gt; $extData);
$model-&gt;update(1, $data); //基于主键的快速更新
</code></pre>
<h3>(2)分表处理</h3>
<p>其次是分表处理，同样考虑到分表的情况，以及不同的表可能配置不同的主键表，而基于主键的CURD又必须要先知道表的主键名才能进行SQL查询。所以，问题就演变成了如何找到表的主键名。这里可以自动匹配，也可以手工指定。自动匹配是智能的，因为当我们更改表的主键时，可以自动同步更新而不需要担心遗漏（虽然这种情况很少发生）。手工指定可以大大减少系统不必要的匹配操作，因为我们开发人员也知道数据库的主键名是什么，但需要手工编写一些代码。在这里，提供了可选的手工指定，即可重写getTableKey($table)来指定你的主键名。</p>
<p>如，当我们的表的主键都固定为id时：</p>
<pre><code class="language-javascript">abstract class Common_Model_NotORM extends PhalApi_Model_NotORM {

    protected function getTableKey($table) {
        return 'id';
    }
}</code></pre>
<hr />
<h4><a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.16%5D-%E9%85%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%EF%BC%9A%E5%86%85%E5%A4%96%E7%BD%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E7%9A%84%E5%AE%8C%E7%BE%8E%E5%88%87%E6%8D%A2.html">下一章</a>#[1.16]-配置读取：内外网环境配置的完美切换</h4>
<h4><a href="/wikis/%5B1.15%5D-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%9F%BA%E4%BA%8ENotORM%E7%9A%84%E4%BD%BF%E7%94%A8%E5%8F%8A%E4%BC%98%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.17%5D-%E6%97%A5%E8%AE%B0%E7%BA%AA%E5%BD%95%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84%E6%97%A5%E8%AE%B0%E6%8E%A5%E5%8F%A3.html">下一章</a></h4>
<hr />
<p><em>Experience by itself teaches nothing... Without theory, experience has no meaning. Without theory, one has no questions to ask. Hence, without theory, there is no learning. – Edwards Deming</em>  </p>
<h2>1.16.1 配置的简单读取</h2>
<p>通常，我们会有以下三个配置文件：  </p>
<pre><code class="language-javascript">dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
└── sys.php
</code></pre>
<p>其中app.php为项目应用配置；dbs.php为分布式存储的数据库配置；sys.php为不同环境下的系统配置。  </p>
<p>在入口文件，注册配置组件服务后：</p>
<pre><code class="language-javascript">//$ vim ./Public/init.php

//配置
DI()-&gt;config = new PhalApi_Config_File(API_ROOT . '/Config');</code></pre>
<p>假设有这样的app.php配置：</p>
<pre><code class="language-javascript">return array(

    'version' =&gt; '1.1.1',

    'email' =&gt; array(
        'address' =&gt; 'chanzonghuang@gmail.com',
    );
);
</code></pre>
<p>我们就可以分别这样根据需要获取配置：</p>
<pre><code class="language-javascript">//app.php里面的全部配置
DI()-&gt;config-&gt;get('app');

//app.php里面的单个配置
DI()-&gt;config-&gt;get('app.version');  //返回：1.1.1

//app.php里面的多级配置
DI()-&gt;config-&gt;get('app.email.address');  //返回：'chanzonghuang@gmail.com'</code></pre>
<p>其他配置文件类似，你也可以随意添加新的配置文件。</p>
<h2>1.16.2 内外环境配置管理的策略</h2>
<p>可以说，在项目开发中，除了我们的代码、数据和文档外，配置也是一块相当重要的组成部分，而且占据着非常重要的位置。最为明显的是，如果配置一旦出错，就很可能影响到整个系统的运行，并且远比修改代码再上线发布速度快（假设我们不用文件存储配置而是以分布式存储在各服务器内存中）。  </p>
<p>但这里将讨论另外一个同样重要的问题，即： <strong>不同环境下不同配置的管理和切换</strong> 。  </p>
<p>现在将不同的策略分说如下。</p>
<h3>(1)支持整包发布的环境变量配置</h3>
<p>此种策略的主要方法就是在PHP代码中读取php-fpm中的ENV环境配置，再对应到linux环境下的profile环境变量，即：</p>
<pre><code>PHP代码  --&gt;    $_ENV环境配置  --&gt;   Linux服务器环境变量/etc/profile</code></pre>
<p>这样的好处莫过于可以支持项目代码的整包发布，而不需要在各个环境（开发环境、测试环境、回归环境、预发布环境、生产环境）来回修改切换配置，同时运维可以更好地保护服务器的账号和密码而不让开发知道。</p>
<p>而这样的不足则是，在对项目进行初次部署时，需要添加以上一系列的配置，而且后期维护也比较复杂麻烦，特别当机器多时。这时可以通过pupple/stacksalt这些运维工具进行自动化管理。但对于开发来说，依然会觉得有点烦锁。</p>
<h3>(2)不同环境，不同入口</h3>
<p>当服务器的账号和密码也是由开发来掌控时，则可以使用这种在代码层次控制的策略。  </p>
<p>我们可以在Public下提供不同的入口，一如：</p>
<pre><code class="language-javascript">$ mkdir ./Public/myapi
$ myapi$ tree
.
├── index.php
└── test.php
</code></pre>
<p>我们有这样不同的入口，客户端在测试时，只需要将入口路径改成：/myapi/test.php?service=XXX.XXX，而在打包发布时只需要将入口路径改成：/myapi/?service=XXX.XXX 即可。也就是将test.php去掉。  </p>
<p>而在服务端，仅需要在这些不同的入口文件，修改一下配置文件目录路径即可：</p>
<pre><code class="language-javascript">//$ vim ./Public/myapi/test.php
DI()-&gt;config = new PhalApi_Config_File(API_ROOT . '/ConfigTest');</code></pre>
<p>另外，也可以通过客户端在请求时带参数来作区分，如带上&amp;env=test或者&amp;env=prod。</p>
<h3>(3)持续集成下的配置管理</h3>
<p>但个人最为建议的还是在 <strong>持续集成</strong> 下作配置管理。  </p>
<p>因为首先，持续集成中的发布应该是经常性的，且应该是自动化的。所以，既然有自动化的支持，我们也应该及早地将配置纳入其中管理。</p>
<p>配置文件不同只要是环境不同，而环境不同所影响的配置文件通常只有sys.php和dbs.php；为此，我们为测试环境和生产环境准备了各自的配置文件，而在发布时自动选择所需要的配置文件。一般地，我们建议生产环境的配置文件以 <strong>.prod</strong> 结尾。所以，这时我们的配置文件可能会是这样：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Config$ tree
.
├── app.php
├── dbs.php
├── dbs.php.prod
├── sys.php
└── sys.php.prod</code></pre>
<p>多了生产环境的配置文件：dbs.php.prod和sys.php.prod。  </p>
<p>再通过发布工具，我们就可以对不同环境的配置文件进行快速选择了。这里以phing为例，说明一下相关的配置和效果。  </p>
<p>如下，在Phing的配置文件build.xml中，在生产环境发布过程中，我们将配置文件进行了替换。</p>
<pre><code class="language-javascript">//$ vim ./build.xml 

        &lt;copy 
            file="./Config/dbs.php.prod" 
            tofile="./Config/dbs.php" 
            overwrite="true" /&gt;
        &lt;copy 
            file="./Config/sys.php.prod" 
            tofile="./Config/sys.php" 
            overwrite="true" /&gt;</code></pre>
<p>执行phing发布后，将会看到对应的这样提示：</p>
<pre><code class="language-javascript">     [copy] Copying 1 file to /home/phapapi/Config
     [copy] Copying 1 file to /home/phapapi/Config</code></pre>
<h2>1.16.3 主从数据库的配置</h2>
<p>通常一般而言，数据库支持分表分库配置，且只有一份，即：  </p>
<pre><code class="language-javascript">./Config/dbs.php</code></pre>
<p>然后，我们这样指定数据库的配置：  </p>
<pre><code class="language-javascript">DI()-&gt;notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};</code></pre>
<p>如果需要从库时，要以参考./Config/dbs.php，创建一个从库的配置拷贝，即：</p>
<pre><code class="language-javascript">cp ./Config/dbs.php ./Config/dbs_slave.php</code></pre>
<p>然后，注册一个从库的notorm：</p>
<pre><code class="language-javascript">DI()-&gt;notormSlave = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs_slave'), $debug); //注意：配置不同
};</code></pre>
<p>最后使用此从库的服务DI()-&gt;notormSlave即可完成对从库的读取操作，用法同DI()-&gt;notorm，这里不再赘述。</p>
<h2>1.16.4 扩展你的配置读取</h2>
<p>虽然上面有不同的配置文件管理策略，但很多实际情况下，我们的配置需要可以随时更改、下发和调整。并且在海量访问，性能要求高的情况下快速读取配置。  </p>
<p>这就要求我们的项目既可以方便维护即时修改，又需要能够快速同步一致更新下发和读取。这样就涉及到了配置更高层的管理：  统一集中式的配置管理，还是分布式的配置管理？  文件存储，还是DB存储，还是MC缓存，还是进驻内存？    </p>
<p>个人认为，性能最优的莫过于配置进驻内存并分布式存储。毕竟作为前端机的PHP内存这块还是相对充裕且可以利用的。但需要把这一条线打通，有点难度。  </p>
<p>这里不过多地谈论配置更多的内容，但在PhalApi框架中，当你根据自己项目的需要实现了自己的配置读取方式后，再次简单的在入口文件重新注册即可。</p>
<h2>1.16.5 使用Yaconf扩展快速读取配置</h2>
<p>首先，非常感谢 @豹 同学的提议：使用Yaconf扩展快速读取配置。  </p>
<p>虽然现在是冬天，有点冷（特别是在北京的同学），而且现在还是凌晨1点25分。但我们希望能继续将开源的热情继续下去。  </p>
<p>以下是刚刚这几个小时努力的成果。  </p>
<h3>(1)前期准备：PHP7 的安装</h3>
<p>具体可以参考：<a href="http://blog.csdn.net/21aspnet/article/details/47708763">Linux环境PHP7.0安装 </a>  </p>
<h3>(2)前期准备：Yaconf扩展的安装</h3>
<p>具体可以参考：<a href="https://github.com/laruence/yaconf">laruence/yaconf</a>  </p>
<h3>(3)使用</h3>
<p>和正常的配置一样，简单如下：  </p>
<pre><code>DI()-&gt;config = new PhalApi_Config_Yaconf();

var_dump(DI()-&gt;config-&gt;get('foo')); //相当于var_dump(Yaconf::get("foo"));

var_dump(DI()-&gt;config-&gt;has('foo')); //相当于var_dump(Yaconf::has("foo"));</code></pre>
<p>以上，假设我们已经有了这样的配置文件：  </p>
<pre><code>$ vim ./test.ini
name="PhalApi"
version="1.3.1"</code></pre>
<p>需要注意的是，如果需要使用Yaconf扩展，需要PHP7的版本支持，但使用基本上和原来PhalApi的思路是完全一样的。<br />
但还有所区别的是，配置文件的目录路径（当然也可以配置成和PhalApi的配置目录一致）、以及配置文件的格式。  </p>
<p>Just have fun!</p>
<hr />
<h4><a href="/wikis/%5B1.15%5D-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C%EF%BC%9A%E5%9F%BA%E4%BA%8ENotORM%E7%9A%84%E4%BD%BF%E7%94%A8%E5%8F%8A%E4%BC%98%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.17%5D-%E6%97%A5%E8%AE%B0%E7%BA%AA%E5%BD%95%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84%E6%97%A5%E8%AE%B0%E6%8E%A5%E5%8F%A3.html">下一章</a>#[1.17]-日记纪录：简化版的日记接口</h4>
<h4><a href="/wikis/%5B1.16%5D-%E9%85%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%EF%BC%9A%E5%86%85%E5%A4%96%E7%BD%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E7%9A%84%E5%AE%8C%E7%BE%8E%E5%88%87%E6%8D%A2.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.18%5D-%E5%BF%AB%E9%80%9F%E5%87%BD%E6%95%B0%EF%BC%9A%E4%BA%BA%E6%80%A7%E5%8C%96%E7%9A%84%E5%85%B3%E6%80%80.html">下一章</a></h4>
<hr />
<h2>PHP日记接口规范</h2>
<p>关于日记系统，PHP规范给出了相当好的说明和定义，请见： <a href="http://www.php-fig.org/psr/psr-3/">Logger Interface</a>  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_2caf170c179348eca546ba5332837d09" alt="apic" />  </p>
<h2>1.17.1 简化版</h2>
<p>但在用过这么多开源框架进行各种项目开发，以及内部框架的开发后，发现实际上日记的分类并没有使用得这么丰富，而通常只是频繁地几类。因此，为了减少不必要的复杂性，这里特地精简为三种，只有：</p>
<ul>
<li>error 系统异常类</li>
<li>info 业务纪录类</li>
<li>debug 开发调试类</li>
</ul>
<p>现分说如下。</p>
<h3>(1)error 系统异常类</h3>
<p>此类是后台开发同学认为 <strong>不应该发生却发生的事情 </strong> ，即我们通常所说的系统异常。<br />
如：调用app store支付的接口失败了，我们需要纪录一下当时的场景，以便复查和给用户补尝其损失。<br />
又如：写入一条纪录到数据纪录，但失败了，我们需要纪录一下，以便排查。  </p>
<p>用法很简单，如：</p>
<pre><code class="language-javascript">//只有描述
DI()-&gt;logger-&gt;error('fail to insert DB');

//描述 + 简单的信息
DI()-&gt;logger-&gt;error('fail to insert DB', 'try to register user dogstar');

//描述 + 当时的上下文数据
$data = array('name' =&gt; 'dogstar', 'password' =&gt; '123456');
DI()-&gt;logger-&gt;error('fail to insert DB', $data);</code></pre>
<p>上面三条纪录，效果类如：</p>
<pre><code class="language-javascript">dogstar@ubuntu:dev.phalapi.com$ tailf ./Runtime/log/201502/20150207.log 
2015-02-07 20:37:55|ERROR|fail to insert DB
2015-02-07 20:37:55|ERROR|fail to insert DB|try to register user dogstar
2015-02-07 20:37:55|ERROR|fail to insert DB|{"name":"dogstar","password":"123456"}</code></pre>
<p>截图效果：
<img src="http://webtools.qiniudn.com/20150411005257_37a0566c79a78c8958d8d00a83c7f840" alt="apic" />  </p>
<h3>(2)info 业务纪录类</h3>
<p>此类通常为业务需要进行必要的操作纪录，以便发生系统问题后的回滚处理、问题排查以及数据统计。<br />
如在有缓存的情况下，可能数据没及时写入数据库而导致数据丢失或者回档，这里可以通过日记简单查看是否可以恢复。以及说明一下操作发生的背景或原由，如通常游戏中用户的经验值添加：  </p>
<pre><code class="language-javascript">//假设：10 + 2 = 12
DI()-&gt;logger-&gt;info('add user exp', array('name' =&gt; 'dogstar', 'before' =&gt; 10, 'addExp' =&gt; 2, 'after' =&gt; 12, 'reason' =&gt; 'help one more phper'));

//对应的LOG
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}</code></pre>
<p>但当哪天我们看到以下的LOG是就会发现系统存在隐藏的BUG：  </p>
<pre><code class="language-javascript">//WTF? 10 + 2 = 11 ???
2015-02-07 20:48:51|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":11,"reason":"help one more phper"}</code></pre>
<p>而当用户玩家来投诉客服时，客服妹子来找到我们后台开发时，我们可以证明得了是系统原因造成了用户丢失1点经验值。<br />
特别地，若我们看到以下的LOG时，不难看出有人在用非法的渠道刷经验：</p>
<pre><code class="language-javascript">2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":12,"addExp":2,"after":14,"reason":"help one more phper"}

....

2015-02-07 20:52:35|INFO|add user exp|{"name":"dogstar","before":998,"addExp":2,"after":1000,"reason":"help one more phper"}</code></pre>
<p>所幸我们有日记并及时发现了，随后如何处理就视具体的项目而定了。但当产品来追问时，我们可以及时给出反馈和做出处理。  </p>
<p>还有更为重要的是 <strong>数据统计</strong> 。这块就App数据分析和统计这块已经有了很好的第三方服务支持，一如：友盟。但我们仍然可以轻松实现自己的数据统计，以便二次确认和定制化。毕竟，总是依赖第三方不是那么轻便，而且存在敏感数据安全问题。<br />
这里简单提供一个上报接口，如：</p>
<pre><code class="language-javascript">    public function getRules() {
        return array(
            'report' =&gt; array(
                'username' =&gt; array('name' =&gt; 'username', 'require' =&gt; true),
                'msg' =&gt; array('name' =&gt; 'msg', 'require' =&gt; true),
            ),
        );
    }

    public function report() {
        DI()-&gt;logger-&gt;info($this-&gt;username, $this-&gt;msg);
    }
</code></pre>
<p>客户端在需要的场景，如用户打开应用，请求：</p>
<pre><code>http://dev.phalapi.com/demo/?service=DogstarTest.Report&amp;username=dogstar&amp;msg=enter%20app</code></pre>
<p>即可看到：</p>
<pre><code class="language-javascript">2015-02-07 21:01:13|INFO|dogstar|enter app</code></pre>
<p>到后期，若我们需要统计用户的登录情况时，可以这样统计：</p>
<pre><code class="language-javascript">$ cat ./Runtime/log/201502/20150207.log | grep "enter app" | awk -F '|' '{print $3}' | sort | uniq -c
     11 dogstar
      5 King
      2 Tom</code></pre>
<h3>(3)debug 开发调试类</h3>
<p>此类为开发调试用，用法如上，这里不再赘述。以下是一些简单的示例： </p>
<pre><code class="language-javascript">DI()-&gt;logger-&gt;debug('just for test');

DI()-&gt;logger-&gt;debug('just for test', '一些其他的描述 ...');

DI()-&gt;logger-&gt;debug('just for test', array('name' =&gt; 'dogstar', 'password' =&gt; '******'));</code></pre>
<h2>1.17.2 使用DI()-&gt;logger-&gt;log()进行更灵活的分类</h2>
<p>若上面的error/info/debug都不能满足项目的需求时，可以这样更灵活地进行日记纪录：</p>
<pre><code class="language-javascript">DI()-&gt;logger-&gt;log('demo', 'add user exp', array('name' =&gt; 'dogstar', 'after' =&gt; 12));
DI()-&gt;logger-&gt;log('test', 'add user exp', array('name' =&gt; 'dogstar', 'after' =&gt; 12));

//对应的日记
2015-02-07 21:13:27|DEMO|add user exp|{"name":"dogstar","before":10,"addExp":2,"after":12,"reason":"help one more phper"}
2015-02-07 21:15:39|TEST|add user exp|{"name":"dogstar","after":12}
</code></pre>
<p>注意到，DI()-&gt;logger-&gt;log()第一个参数为日记分类的名称，在写入日记时会自动转换为大写。其接口函数签名为：  </p>
<pre><code class="language-javascript">    /**
     * 日记纪录
     *
     * 可根据不同需要，将日记写入不同的媒介
     *
     * @param string $type 日记类型，如：info/debug/error, etc
     * @param string $msg 日记关键描述
     * @param string/array $data 场景上下文信息
     * @return NULL
     */
    abstract public function log($type, $msg, $data);</code></pre>
<h2>1.17.3 别忘记了日记的级别设置</h2>
<p>上面的三类日记分别对应的标识为：</p>
<ul>
<li>error 系统异常类：PhalApi_Logger::LOG_LEVEL_ERROR</li>
<li>info 业务纪录类：PhalApi_Logger::LOG_LEVEL_INFO</li>
<li>debug 开发调试类：PhalApi_Logger::LOG_LEVEL_DEBUG</li>
</ul>
<p>根据不同的项目在不同环境下的需要，我们可以这样有选择性地将需要的日记分类写入保存：</p>
<pre><code class="language-javascript">//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime',
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);</code></pre>
<p>多个日记级别需要使用或运算进行组合。</p>
<h2>1.17.4 扩展你的日记存储</h2>
<p>普遍情况下，我们认为将日记存放在文件是比较合理的，因为便于查看、管理和统计。当然，如果你的项目需要将日记纪录在数据中，也可以快速扩展实现。如实现数据库的存储思路：  </p>
<pre><code class="language-javascript">//$ vim ./Apps/Common/Logger/DB.php

class Common_Logger_DB extends PhalApi_Logger {

    public function log($type, $msg, $data) {
        //TODO 数据库的日记写入 ...
    } </code></pre>
<p>随后，利用DI注册一下即可：</p>
<pre><code class="language-javascript">//日记纪录 - DB
DI()-&gt;logger = new Common_Logger_DB($dbConfig,
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);</code></pre>
<hr />
<h4><a href="/wikis/%5B1.16%5D-%E9%85%8D%E7%BD%AE%E8%AF%BB%E5%8F%96%EF%BC%9A%E5%86%85%E5%A4%96%E7%BD%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E7%9A%84%E5%AE%8C%E7%BE%8E%E5%88%87%E6%8D%A2.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.18%5D-%E5%BF%AB%E9%80%9F%E5%87%BD%E6%95%B0%EF%BC%9A%E4%BA%BA%E6%80%A7%E5%8C%96%E7%9A%84%E5%85%B3%E6%80%80.html">下一章</a>#[1.18]-快速函数：人性化的关怀</h4>
<h4><a href="/wikis/%5B1.17%5D-%E6%97%A5%E8%AE%B0%E7%BA%AA%E5%BD%95%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84%E6%97%A5%E8%AE%B0%E6%8E%A5%E5%8F%A3.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.19%5D-DI%E6%9C%8D%E5%8A%A1%E9%80%9F%E6%9F%A5%EF%BC%9A%E5%90%84%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E4%B8%80%E8%A7%88%E8%A1%A8.html">下一章</a></h4>
<hr />
<h2>1.18.1 提供快速函数的缘由</h2>
<p>很多人，很多框架都在说遵循的是OOP，实际上很多都是面向过程的开发，或者是很呆板的面向对象。  </p>
<p>而且你也会发现，面向对象和面向过程在某方面是相互补充的。而且，面向对象里面很痛苦的改动在面向过程中可以很容易实现，反之亦然。如面向对象中添加一个类成员变量（新数据）很简单，但在抽象父类中添加一个抽象方法（新方法）却很痛苦；而在面向过程中添加一个全局变量（新数据）很痛苦，但要添加一个新方法却很简单。  </p>
<p>所以，没有绝对的好与不好，关键在于如何设计和如何使用。而PhalApi则遵循敏捷开发的思想，为了更多的接口开发人员减少代码编写和记忆的痛苦、体验开发的乐趣，我们思虑衡量再三，提供了以下的快速函数。</p>
<h2>1.18.2 目前已有的快速函数</h2>
<h3>(1)获取全局DI实例的函数：DI()</h3>
<p>为了定位获得全部服务资源，我们需要一个统一的入口，在这里即为PhalApi_DI的全局实例。<br />
我们可以这样原始获取：</p>
<pre><code class="language-javascript">$di = PhalApi_DI::one();</code></pre>
<p>也可以这样快速获取：</p>
<pre><code class="language-javascript">$di = DI();</code></pre>
<p>也就是说：</p>
<pre><code class="language-javascript">function DI() {
    return PhalApi_DI::one();
}</code></pre>
<h3>(2)国际化翻译函数：T()</h3>
<p>在真正需要进行翻译的项目中，翻译函数的调用显然会到处可见。为了方便编写代码，这里取Translator的首写字母T作为快速函数的名字，在需要进行翻译时只需要调用T()，即可：</p>
<pre><code class="language-javascript">$msg = T('hello world');

$msg = T('hello {name}', array('name' =&gt; 'dogstar'));</code></pre>
<p>对应地：</p>
<pre><code class="language-javascript">function T($msg, $params = array()) {
    return PhalApi_Translator::get($msg, $params);
}</code></pre>
<hr />
<h4><a href="/wikis/%5B1.17%5D-%E6%97%A5%E8%AE%B0%E7%BA%AA%E5%BD%95%EF%BC%9A%E7%AE%80%E5%8C%96%E7%89%88%E7%9A%84%E6%97%A5%E8%AE%B0%E6%8E%A5%E5%8F%A3.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.19%5D-DI%E6%9C%8D%E5%8A%A1%E9%80%9F%E6%9F%A5%EF%BC%9A%E5%90%84%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E4%B8%80%E8%A7%88%E8%A1%A8.html">下一章</a>#[1.19]-DI服务速查：各资源服务一览表</h4>
<h4><a href="/wikis/%5B1.18%5D-%E5%BF%AB%E9%80%9F%E5%87%BD%E6%95%B0%EF%BC%9A%E4%BA%BA%E6%80%A7%E5%8C%96%E7%9A%84%E5%85%B3%E6%80%80.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5b1.20%5d-DB%e6%93%8d%e4%bd%9c%ef%bc%9a%e6%95%b0%e6%8d%ae%e5%ba%93%e5%9f%ba%e6%9c%ac%e6%93%8d%e4%bd%9c%e9%80%9f%e6%9f%a5.html">下一章</a></h4>
<hr />
<p>“完成，好过完美。”  -- 脸谱书文化之一  </p>
<h2>1.19.1 一览表</h2>
<table>
<thead>
<tr>
<th>变量名称</th>
<th>是否启动时自动注册</th>
<th>是否必须</th>
<th>接口/类</th>
<th>作用说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>loader</td>
<td>否</td>
<td>是</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Loader.html">PhalApi_Loader</a></td>
<td>自动加载：负责PEAR风格下类的自动加载，需要手动注册，指定项目路径</td>
</tr>
<tr>
<td>config</td>
<td>否</td>
<td>是</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Config.html">PhalApi_Config</a></td>
<td>配置：负责项目配置的读取，需要手动注册，指定存储媒介，默认是<a href="http://www.phalapi.net/docs/classes/PhalApi_Config_File.html">PhalApi_Config_File</a></td>
</tr>
<tr>
<td>logger</td>
<td>否</td>
<td>是</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Logger.html">PhalApi_Logger</a></td>
<td>日记纪录：负责日记的写入，需要手动注册，指定日记级别和存储媒介，默认是<a href="http://www.phalapi.net/docs/classes/PhalApi_Logger_File.html">PhalApi_Logger_File</a></td>
</tr>
<tr>
<td>request</td>
<td>是</td>
<td>是</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request.html">PhalApi_Request</a></td>
<td>接口参数请求：用于收集接口请求的参数</td>
</tr>
<tr>
<td>response</td>
<td>是</td>
<td>是</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Response.html">PhalApi_Response</a></td>
<td>结果响应：用于输出返回给客户端的结果，默认为<a href="http://www.phalapi.net/docs/classes/PhalApi_Response_Json.html">PhalApi_Response_Json</a></td>
</tr>
<tr>
<td>notorm</td>
<td>否</td>
<td>推荐</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_DB_NotORM.html">PhalApi_DB_NotORM</a></td>
<td>数据操作：基于NotORM的DB操作，需要手动注册，指定数据库配置</td>
</tr>
<tr>
<td>cache</td>
<td>否</td>
<td>推荐</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Cache.html">PhalApi_Cache</a></td>
<td>缓存：实现缓存读写，需要手动注册，指定缓存</td>
</tr>
<tr>
<td>filter</td>
<td>否</td>
<td>推荐</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Filter.html">PhalApi_Filter</a></td>
<td>拦截器：实现签名验证、权限控制等操作</td>
</tr>
<tr>
<td>crypt</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Crypt.html">PhalApi_Crypt</a></td>
<td>对称加密：实现对称加密和解密，需要手动注册</td>
</tr>
<tr>
<td>curl</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_CUrl.html">PhalApi_CUrl</a></td>
<td>CURL请求类：通过curl实现的快捷方便的接口请求类，需要手动注册</td>
</tr>
<tr>
<td>cookie</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Cookie.html">PhalApi_Cookie</a></td>
<td>COOKIE的操作</td>
</tr>
<tr>
<td>debug</td>
<td>否</td>
<td>否</td>
<td>boolean</td>
<td>应用级的调试开关，通常可从配置读取，暂时框架未用到</td>
</tr>
<tr>
<td>_formatterArray</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Array.html">PhalApi_Request_Formatter_Array</a></td>
<td>数组格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterBoolean</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Boolean.html">PhalApi_Request_Formatter_Boolean</a></td>
<td>布尔值格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterCallable</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Callable.html">PhalApi_Request_Formatter_Callable</a></td>
<td>回调格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterDate</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Date.html">PhalApi_Request_Formatter_Date</a></td>
<td>日期格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterEnum</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Enum.html">PhalApi_Request_Formatter_Enum</a></td>
<td>枚举格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterFile</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_File.html">PhalApi_Request_Formatter_File</a></td>
<td>上传文件格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterFloat</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Float.html">PhalApi_Request_Formatter_Float</a></td>
<td>浮点数格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterInt</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_Int.html">PhalApi_Request_Formatter_Int</a></td>
<td>整数格式化服务（系统内部使用）</td>
</tr>
<tr>
<td>_formatterString</td>
<td>否</td>
<td>否</td>
<td><a href="http://www.phalapi.net/docs/classes/PhalApi_Request_Formatter_String.html">PhalApi_Request_Formatter_String</a></td>
<td>字符串格式化服务（系统内部使用）</td>
</tr>
</tbody>
</table>
<h2>1.19.2 DI服务是否已注册的判断误区</h2>
<h3>(1)错误的判断方法</h3>
<p>当需要判断一个DI服务是否已被注册，出于常识会这样判断：  </p>
<pre><code>if (isset(DI()-&gt;cache)) {</code></pre>
<p>但这样的判断永远为false，不管注册与否。  </p>
<p>追其原因在于，DI类使用了魔法方法的方式来提供类成员属性，并存放于PhalApi_DI::$data中。  </p>
<p>这就导致了如果直接使用isset(DI()-&gt;cache)的话，首先不会触发魔法方法 PhalApi_DI::__get($name)的调用，其次也确实没有PhalApi_DI::$cache这个成员属性，最终判断是否存在时都为false。  </p>
<p>简单来说，以下两种判断，永远都为false：  </p>
<pre><code>//永远为false
var_dump(isset(DI()-&gt;XXX));
var_dump(!empty(DI()-&gt;XXX));</code></pre>
<h3>(2)正确判断的写法：先获取，再判断</h3>
<p>正确的用法应该是：  </p>
<pre><code>//先获取，再判断
$XXX = DI()-&gt;XXX;
var_dump(isset($XXX));
var_dump(!empty($XXX));</code></pre>
<p>这里，可能会对大家带来一定的困惑，所以在这里特别进行说明。也对框架中这一点不合理的设计深感抱歉。  </p>
<hr />
<h4><a href="/wikis/%5B1.18%5D-%E5%BF%AB%E9%80%9F%E5%87%BD%E6%95%B0%EF%BC%9A%E4%BA%BA%E6%80%A7%E5%8C%96%E7%9A%84%E5%85%B3%E6%80%80.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5b1.20%5d-DB%e6%93%8d%e4%bd%9c%ef%bc%9a%e6%95%b0%e6%8d%ae%e5%ba%93%e5%9f%ba%e6%9c%ac%e6%93%8d%e4%bd%9c%e9%80%9f%e6%9f%a5.html">下一章</a></h4>
<h1>[1.20]-DB操作：数据库基本操作速查</h1>
<h4><a href="/wikis/%5B1.19%5D-DI%E6%9C%8D%E5%8A%A1%E9%80%9F%E6%9F%A5%EF%BC%9A%E5%90%84%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E4%B8%80%E8%A7%88%E8%A1%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.21%5D-%E7%B1%BB%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD%EF%BC%9A%E9%81%B5%E5%BE%AAPEAR%E5%8C%85%E7%9A%84%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.html">下一章</a></h4>
<hr />
<p><em>一花一世界，<br />
一沙一天国，<br />
君掌盛无边，<br />
刹那含永劫。</em></p>
<p><em>-- 《天真的预兆》</em></p>
<h2>1.20.1 前提</h2>
<p>为了让大家更为明确数据库NotORM的使用，我们假设有以下数据库表：</p>
<pre><code>-- ----------------------------
-- Table structure for `tbl_user`
-- ----------------------------
DROP TABLE IF EXISTS `tbl_user`;
CREATE TABLE `tbl_user` (
  `id` int(11) NOT NULL,
  `name` varchar(45) DEFAULT NULL,
  `age` int(3) DEFAULT NULL,
  `note` varchar(45) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of tbl_user
-- ----------------------------
INSERT INTO `tbl_user` VALUES ('1', 'dogstar', '18', 'oschina', '2015-12-01 09:42:31');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', '21', 'USA', '2015-12-08 09:42:38');
INSERT INTO `tbl_user` VALUES ('3', 'King', '100', 'game', '2015-12-23 09:42:42');</code></pre>
<p>并且，假设我们已获得了tbl_user表对应的notorm实例$user，此NotORM表实例可从两种方式获得：  </p>
<ul>
<li>1、使用原生态的notorm，即：$user = DI()-&gt;notorm-&gt;user  </li>
<li>2、使用PhalApi_Model_NotORM基类的方式，即：$user = $this-&gt;getORM()，如：</li>
</ul>
<pre><code>&lt;?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'user';
    }

    public function doSth() {
        $user = $this-&gt;getORM(); //获取NotORM表实例
    }
}</code></pre>
<h2>写在前面的话</h2>
<p>NotORM的实例是有内部状态的，因为在开发过程中，需要特别注意何时需要保留状态（使用同一个实例）、何时不需要保留状态。即：  </p>
<p>保留状态的写法：  </p>
<pre><code>$user = $notorm-&gt;user;  //获取一个新的实例
$user-&gt;where('age &gt; ?', 18);
$user-&gt;where('name LIKE ?', '%dog%');  //相当于age &gt; 18 AND name LIKE '%dog%'</code></pre>
<p>不保留状态的写法：</p>
<pre><code>$user = $notorm-&gt;user;  //获取一个新的实例
$user-&gt;where('age &gt; ?', 18);

$user = $notorm-&gt;user;  //重新获取新的实例
$user-&gt;where('name LIKE ?', '%dog%');  //此时只有 name LIKE '%dog%'</code></pre>
<p>关于这两者的使用场景，下面在进行说明时会特别提及。<br />
下面，就让我们结合实例来尝试一下数据库的操作吧！</p>
<h3>调试模式</h3>
<h4>(1)打印SQL语句</h4>
<p>在请求接口时，当需要查看当前接口所执行的SQL语句时，可以添加参数<code>&amp;__sql__=1</code>。如请求：</p>
<pre><code>http://demo.phalapi.net/?service=User.getBaseInfo&amp;user_id=1&amp;__sql__=1</code></pre>
<p>会返回类似这样：</p>
<pre><code>[1 - 0.00035s]SELECT * FROM tbl_user WHERE (id = ?); -- 1&lt;br&gt;
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}</code></pre>
<h4>(2)显示数据库连接错误的详细信息</h4>
<p>普通情况下，数据库连接失败时会这样提示：</p>
<pre><code>{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败"
}</code></pre>
<p>考虑到生产环境不方便爆露服务器的相关信息，故这样简化提示。当在开发过程中，需要定位数据库连接失败的原因时，可使用debug调试模式。<br />
开启调试模式的方法有两种：  </p>
<ul>
<li>1、在请求接口时添加 <code>&amp;__debug__=1</code>，开启本次调试  </li>
<li>2、将配置文件 <code>./Config/sys.php</code>中的debug设置为true，开启全局调试  </li>
</ul>
<p>开启后，当再次失败时，会看到类似这样的提示：  </p>
<pre><code>{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: 数据库db_demo连接失败，异常码：1045，错误原因：SQLSTATE[28000] [1045] Access denied for user 'root'@'localhost' (using password: NO)"
}</code></pre>
<p>然后，便可根据具体的错误提示进行排查解决。  </p>
<h3>默认表名与指定表名</h3>
<p><strong>请注意：PhalApi v1.3.4 版本以上有效。</strong>  </p>
<p>当继承PhalApi_Model_NotORM时，如编写Model_User类：  </p>
<pre><code>&lt;?php
class Model_User extends PhalApi_Model_NotORM {
}</code></pre>
<p>则默认的表名为：<code>user</code>。默认表名的自动匹配规则是：取<code>Model_</code>后面部分的字符全部转小写。转化后框架会另外加上表前缀。    </p>
<p>在以下场景下，需要重写此方法以指定表名：  </p>
<ul>
<li>自动匹配的表名与实际表名不符</li>
<li>存在分表</li>
<li>Model类名不含有Model_  </li>
</ul>
<p>如，当Model_User类对应的表名为：my_user表时，可使用<code>getTableName($id)</code>重新指定表名：  </p>
<pre><code>&lt;?php
class Model_User extends PhalApi_Model_NotORM {
    protected function getTableName($id) {
        return 'my_user'; 
    }
}</code></pre>
<h2>1.20.2 基本操作</h2>
<h3>(1)SELECT</h3>
<h4>单个字段：</h4>
<pre><code>// SELECT id FROM `tbl_user`
$user-&gt;select('id') </code></pre>
<h4>多个字段获取：</h4>
<pre><code>// SELECT id, name, age FROM `tbl_user`
$user-&gt;select('id, name, age') </code></pre>
<h4>字段别名获取：</h4>
<pre><code>// SELECT id, name, MAX(age) AS max_age FROM `tbl_user`
$user-&gt;select('id, name, MAX(age) AS max_age') </code></pre>
<h4>全部字段（缺省）获取：</h4>
<pre><code>// SELECT * FROM `tbl_user`
$user-&gt;select('*') </code></pre>
<h3>(2)WHERE</h3>
<h4>单个字段查询：</h4>
<pre><code>// WHERE id = 1
$user-&gt;where('id', 1)
$user-&gt;where('id = ?', 1)
$user-&gt;where(array('id', 1))</code></pre>
<h4>多个字段查询：</h4>
<pre><code>// WHERE id &gt; 1 AND age &gt; 18
$user-&gt;where('id &gt; ?', 1)-&gt;where('age &gt; ?', 18)
$user-&gt;and('id &gt; ?', 1)-&gt;and('age &gt; ?', 18)
$user-&gt;where('id &gt; ? AND age &gt; ?', 1, 18)
$user-&gt;where(array('id &gt; ?' =&gt; 1, 'age &gt; ?' =&gt; 10))

// WHERE name = 'dogstar' AND age = 18
$user-&gt;where(array('name' =&gt; 'dogstar', 'age' =&gt; 18))

// WHERE name = 'dogstar' OR age = 18
$user-&gt;or('name', 'dogstar')-&gt;or('age', 18)

// WHERE ((name = ? OR id = ?)) AND (note = ?) -- 'dogstar', '1', 'xxx'
#实现方式1：使用and拼接
$user-&gt;where('(name = ? OR id = ?)', 'dogstar', '1')-&gt;and('note = ?', 'xxx')
#实现方式2：只使用where，不用数组传参
$user-&gt;where('(name = ? OR id = ?) AND note = ?', 'dogstar', '1', 'xxx')
#实现方式3：只使用where，用下标为顺序数字的数组传参
$user-&gt;where('(name = ? OR id = ?) AND note = ?', array('dogstar', '1', 'xxx'))
#实现方式4：只使用where，用下标为标识符的数组传参
$user-&gt;where('(name = :name OR id = :id) AND note = :note', array(':name' =&gt; 'dogstar', ':id' =&gt; '1', ':note' =&gt; 'xxx'))</code></pre>
<h4>IN查询：</h4>
<pre><code>// WHERE id IN (1, 2, 3)
$user-&gt;where('id', array(1, 2, 3))

// WHERE id NOT IN (1, 2, 3)
$user-&gt;where('NOT id', array(1, 2, 3))

// WHERE (id, age) IN ((1, 18), (2, 20))
$user-&gt;where('(id, age)', array(array(1, 18), array(2, 20)))</code></pre>
<h4>模糊匹配查询：</h4>
<pre><code>// WHERE name LIKE '%dog%'
$user-&gt;where('name LIKE ?', '%dog%')</code></pre>
<h4>NULL判断查询：</h4>
<pre><code>// WHERE (name IS NULL)
$user-&gt;where('name', null)</code></pre>
<p>对于非空的判断，则可以：  </p>
<pre><code>// WHERE (name IS NOT ?) LIMIT 1; -- NULL
$user-&gt;where('name IS NOT ?', null)</code></pre>
<h3>(3)ORDER BY</h3>
<h4>单个字段排序：</h4>
<pre><code>// ORDER BY age
$user-&gt;order('age')

// ORDER BY age DESC
$user-&gt;order('age DESC')</code></pre>
<h4>多个字段排序：</h4>
<pre><code>// ORDER BY id, age DESC
$user-&gt;order('id')-&gt;order('age DESC')
$user-&gt;order('id, age DESC')</code></pre>
<h3>(4)LIMIT</h3>
<h4>按数量限制：</h4>
<pre><code>// LIMIT 10
$user-&gt;limit(10)</code></pre>
<h4>按数量和偏移量限制（请注意：先偏移量、再数量）：</h4>
<pre><code>// LIMIT 2,10
$user-&gt;limit(2, 10)</code></pre>
<h3>(5)GROUP BY和HAVING</h3>
<h4>不带HAVING：</h4>
<pre><code>// GROUP BY note
$user-&gt;group('note')</code></pre>
<h4>带HAVING：</h4>
<pre><code>// GROUP BY note HAVING age &gt; 10
$user-&gt;group('note', 'age &gt; 10')</code></pre>
<h2>1.20.3 CURD之查询类（Retrieve）</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>说明</th>
<th>示例</th>
<th>备注</th>
<th>是否PhalApi新增</th>
</tr>
</thead>
<tbody>
<tr>
<td>fetch()</td>
<td>循环获取每一行</td>
<td>while($row = $user-&gt;fetch()) { //... }</td>
<td>否</td>
</tr>
<tr>
<td>fetchOne()</td>
<td>只获取第一行</td>
<td>$row = $user-&gt;where('id', 1)-&gt;fetchOne();</td>
<td>等效于fetchRow()</td>
<td>是</td>
</tr>
<tr>
<td>fetchRow()</td>
<td>只获取第一行</td>
<td>$row = $user-&gt;where('id', 1)-&gt;fetchRow();</td>
<td>等效于fetchOne()</td>
<td>是</td>
</tr>
<tr>
<td>fetchPairs()</td>
<td>获取键值对</td>
<td>$row = $user-&gt;fetchPairs('id', 'name');</td>
<td>第二个参数为空时，可取多个值，并且多条纪录</td>
<td>否</td>
</tr>
<tr>
<td>fetchAll()</td>
<td>获取全部的行</td>
<td>$rows = $user-&gt;where('id', array(1, 2, 3))-&gt;fetchAll();</td>
<td>等效于fetchRows()</td>
<td>是</td>
</tr>
<tr>
<td>fetchRows()</td>
<td>获取全部的行</td>
<td>$rows = $user-&gt;where('id', array(1, 2, 3))-&gt;fetchRows();</td>
<td>等效于fetchAll()</td>
<td>是</td>
</tr>
<tr>
<td>queryAll()</td>
<td>复杂查询下获取全部的行，默认下以主键为下标</td>
<td>$rows = $user-&gt;queryAll($sql, $parmas);</td>
<td>等效于queryRows()</td>
<td>是</td>
</tr>
<tr>
<td>queryRows()</td>
<td>复杂查询下获取全部的行，默认下以主键为下标</td>
<td>$rows = $user-&gt;queryRows($sql, $parmas);</td>
<td>等效于queryAll()</td>
<td>是</td>
</tr>
<tr>
<td>count()</td>
<td>查询总数</td>
<td>$total = $user-&gt;count('id');</td>
<td>第一参数可省略</td>
<td>否</td>
</tr>
<tr>
<td>min()</td>
<td>取最小值</td>
<td>$minId = $user-&gt;min('id');</td>
<td>否</td>
</tr>
<tr>
<td>max()</td>
<td>取最大值</td>
<td>$maxId = $user-&gt;max('id');</td>
<td>否</td>
</tr>
<tr>
<td>sum()</td>
<td>计算总和</td>
<td>$sum = $user-&gt;sum('age');</td>
<td>否</td>
</tr>
</tbody>
</table>
<h4>循环获取每一行（多个字段）：</h4>
<pre><code>// SELECT id, name FROM tbl_user WHERE (age &gt; 18);
$user = $notorm-&gt;user-&gt;select('id, name')-&gt;where('age &gt; 18');
while($row = $user-&gt;fetch()) {
     var_dump($row);
}

// 输出
array(2) {
  ["id"]=&gt;
  string(1) "2"
  ["name"]=&gt;
  string(3) "Tom"
}
array(2) {
  ["id"]=&gt;
  string(1) "3"
  ["name"]=&gt;
  string(4) "King"
}</code></pre>
<h4>循环获取每一行（单个字段）：</h4>
<pre><code>// SELECT id, name FROM tbl_user WHERE (age &gt; 18);
$user = $notorm-&gt;user-&gt;select('id, name')-&gt;where('age &gt; 18');
while($row = $user-&gt;fetch('name')) { // 指定获取name这列，但此字段须在select里
     var_dump($row);
}

// 输出
string(3) "Tom"
string(4) "King"</code></pre>
<h4>循环获取每一行（错误的用法，注意！会死循环）：</h4>
<pre><code>while($row = $notorm-&gt;user-&gt;select('id, name')-&gt;where('age &gt; 18')-&gt;fetch('name')) {
     var_dump($row);
}</code></pre>
<h4>只获取第一行（多个字段）：</h4>
<pre><code>// SELECT id, name FROM tbl_user WHERE (age &gt; 18) LIMIT 1;
$rs = $user-&gt;select('id, name')-&gt;where('age &gt; 18')-&gt;fetchOne();  //等同fetchRow()
var_dump($rs);

// 输出
array(2) {
  ["id"]=&gt;
  string(1) "2"
  ["name"]=&gt;
  string(3) "Tom"
}</code></pre>
<h4>只获取第一行（单个字段）：</h4>
<pre><code>var_dump($user-&gt;fetchOne('name'));  // 输出 string(3) "Tom"
var_dump($user-&gt;fetchRow('name'));  // 等同输出 string(3) "Tom"</code></pre>
<h4>获取键值对（多个字段）：</h4>
<pre><code>// SELECT id, name, age FROM tbl_user LIMIT 2;
$rs = $user-&gt;select('name, age')-&gt;limit(2)-&gt;fetchPairs('id'); //指定以ID为KEY
var_dump($rs);

// 输出
array(2) {
  [1]=&gt;
  array(3) {
    ["id"]=&gt;
    string(1) "1"
    ["name"]=&gt;
    string(7) "dogstar"
    ["age"]=&gt;
    string(2) "18"
  }
  [2]=&gt;
  array(3) {
    ["id"]=&gt;
    string(1) "2"
    ["name"]=&gt;
    string(3) "Tom"
    ["age"]=&gt;
    string(2) "21"
  }
}</code></pre>
<h4>获取键值对（单个字段）：</h4>
<pre><code>// SELECT id, name FROM tbl_user LIMIT 2
var_dump($user-&gt;limit(2)-&gt;fetchPairs('id', 'name')); //通过第二个参数，指定VALUE的列

// 输出
array(2) {
  [1]=&gt;
  string(7) "dogstar"
  [2]=&gt;
  string(3) "Tom"
}</code></pre>
<h4>获取全部的行：</h4>
<pre><code>// SELECT * FROM tbl_user
var_dump($user-&gt;fetchAll());  //全部表数据输出，输出结果略，相当于$user-&gt;fetchRows()</code></pre>
<h4>复杂查询下获取全部的行（1.3.1及以前版本默认下以主键为下标，1.3.1以后的版本则默认采用数组方式）：</h4>
<pre><code>// SELECT name FROM tbl_user WHERE age &gt; :age LIMIT 1
$sql = 'SELECT name FROM tbl_user WHERE age &gt; :age LIMIT 1';
$params = array(':age' =&gt; 18);
$rs = $user-&gt;queryAll($sql, $params);
var_dump($rs);

// 输出
array(1) {
  [0]=&gt;
  array(1) {
    ["name"]=&gt;
    string(3) "Tom"
  }
}

// 也可以这样。。。
$sql = 'SELECT name FROM tbl_user WHERE age &gt; ? LIMIT 1';  //使用问号表示变量
$params = array(18);
$rs = $user-&gt;queryRows($sql, $params); //使用queryRows()别名
var_dump($rs);</code></pre>
<p>请注意：使用上面这种方式进行查询，需要手动填写完整的表名字，并且需要通过某个表的实例来运行。  </p>
<h4>取最小值：</h4>
<pre><code>// SELECT MIN(age) FROM tbl_user
var_dump($user-&gt;min('age'));

// 输出
string(2) "18"</code></pre>
<h4>取最大值：</h4>
<pre><code>// SELECT MAX(age) FROM tbl_user
var_dump($user-&gt;max('age'));

// 输出
string(3) "100"</code></pre>
<h4>计算总和：</h4>
<pre><code>// SELECT SUM(age) FROM tbl_user
var_dump($user-&gt;sum('age'));

// 输出
string(3) "139"</code></pre>
<h2>1.20.4 CURD之插入类（Create）</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>说明</th>
<th>示例</th>
<th>备注</th>
<th>是否PhalApi新增</th>
</tr>
</thead>
<tbody>
<tr>
<td>insert()</td>
<td>插入数据</td>
<td>$user-&gt;insert($data);</td>
<td>原生态操作需要再调用insert_id()获取插入的ID</td>
<td>否</td>
</tr>
<tr>
<td>insert_multi()</td>
<td>批量插入</td>
<td>$user-&gt;insert_multi($rows);</td>
<td>可批量插入</td>
<td>否</td>
</tr>
<tr>
<td>insert_update()</td>
<td>插入/更新</td>
<td>接口签名：insert_update(array $unique, array $insert, array $update = array()</td>
<td>不存时插入，存在时更新</td>
<td>否</td>
</tr>
</tbody>
</table>
<h4>插入数据</h4>
<pre><code>// INSERT INTO tbl_user (id, name, age, note) VALUES (4, 'PhalApi', 1, 'framework')
$data = array('id' =&gt; 4, 'name' =&gt; 'PhalApi', 'age' =&gt; 1, 'note' =&gt; 'framework');
$user-&gt;insert($data);
$id = $user-&gt;insert_id(); //必须是同一个实例，方能获取到新插入的行ID，且表必须设置了自增
var_dump($id); //新增的ID

//使用Model的写法
$model = new Model_User();
var_dump($model-&gt;insert($data)); //输出新增的ID</code></pre>
<h4>批量插入：</h4>
<pre><code>// INSERT INTO tbl_user (name, age, note) VALUES ('A君', 12, 'AA'), ('B君', 14, 'BB'), ('C君', 16, 'CC')
$rows = array(
    array('name' =&gt; 'A君', 'age' =&gt; 12, 'note' =&gt; 'AA'),
    array('name' =&gt; 'B君', 'age' =&gt; 14, 'note' =&gt; 'BB'),
    array('name' =&gt; 'C君', 'age' =&gt; 16, 'note' =&gt; 'CC'),
);
$rs = $user-&gt;insert_multi($rows);
var_dump($rs);

// 输出
int(3) //成功插入的条数</code></pre>
<h4>插入/更新：</h4>
<pre><code>// INSERT INTO tbl_user (id, name, age, note) VALUES (8, 'PhalApi', 1, 'framework') ON DUPLICATE KEY UPDATE age = 2
$unique = array('id' =&gt; 8);
$insert = array('id' =&gt; 8, 'name' =&gt; 'PhalApi', 'age' =&gt; 1, 'note' =&gt; 'framework');
$update = array('age' =&gt; 2);
$rs = $user-&gt;insert_update($unique, $insert, $update);
var_dump($rs); //输出影响的行数</code></pre>
<h2>1.20.5 CURD之更新类（Update）</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>说明</th>
<th>示例</th>
<th>备注</th>
<th>是否PhalApi新增</th>
</tr>
</thead>
<tbody>
<tr>
<td>update()</td>
<td>更新数据</td>
<td>$user-&gt;where('id', 1)-&gt;update($data);</td>
<td>更新异常时返回fals，数据无变化时返回0，成功更新返回1</td>
<td>否</td>
</tr>
</tbody>
</table>
<h4>更新数据：</h4>
<pre><code>// UPDATE tbl_user SET age = 2 WHERE (name = 'PhalApi');
$data = array('age' =&gt; 2);
$rs = $user-&gt;where('name', 'PhalApi')-&gt;update($data);
var_dump($rs);

// 输出
int(1) //正常影响的行数
int(0) //无更新，或者数据没变化
boolean(false) //更新异常、失败</code></pre>
<h4>更新数据（+1）：</h4>
<pre><code>// UPDATE tbl_user SET age = age + 1 WHERE (name = 'PhalApi')
$rs = $user-&gt;where('name', 'PhalApi')-&gt;update(array('age' =&gt; new NotORM_Literal("age + 1")));
var_dump($rs); //输出影响的行数</code></pre>
<h2>1.20.6 CURD之删除类（Delete）</h2>
<table>
<thead>
<tr>
<th>操作</th>
<th>说明</th>
<th>示例</th>
<th>备注</th>
<th>是否PhalApi新增</th>
</tr>
</thead>
<tbody>
<tr>
<td>delete()</td>
<td>删除</td>
<td>$user-&gt;where('id', 1)-&gt;delete();</td>
<td>禁止无where条件的删除操作</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>按条件删除，返回影响的行数：</p>
<pre><code>// DELETE FROM tbl_user WHERE (id = 404);
$user-&gt;where('id', 404)-&gt;delete();</code></pre>
<p><strong> 注意：</strong> 如果是全表删除，框架将会禁止，并抛出异常。如：</p>
<pre><code>// Exception: sorry, you can not delete the whole table
$user-&gt;delete();</code></pre>
<h2>参考</h2>
<p>更多请参考 <a href="http://www.notorm.com/#api">NotORM官网接口说明</a> </p>
<hr />
<h4><a href="/wikis/%5B1.19%5D-DI%E6%9C%8D%E5%8A%A1%E9%80%9F%E6%9F%A5%EF%BC%9A%E5%90%84%E8%B5%84%E6%BA%90%E6%9C%8D%E5%8A%A1%E4%B8%80%E8%A7%88%E8%A1%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.21%5D-%E7%B1%BB%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD%EF%BC%9A%E9%81%B5%E5%BE%AAPEAR%E5%8C%85%E7%9A%84%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.html">下一章</a></h4>
<h1>[1.21]-类的自动加载：遵循PEAR包的命名规范</h1>
<h4><a href="/wikis/%5b1.20%5d-DB%e6%93%8d%e4%bd%9c%ef%bc%9a%e6%95%b0%e6%8d%ae%e5%ba%93%e5%9f%ba%e6%9c%ac%e6%93%8d%e4%bd%9c%e9%80%9f%e6%9f%a5.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.22%5D-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AD%BE%E5%90%8D%E8%A7%84%E5%88%99.html">下一章</a></h4>
<hr />
<h2>1.21.1 PhalApi自动加载的机制</h2>
<p>首先，PhalApi的自动加载是简单的；其次，PhalApi不强制只使用我们框架的机制。  </p>
<p>有些框架，单单在类文件的自动加载这一块就弄得非常复杂，以致开发同步需要在这些框架添加一些自己的类代码时，往往不知要怎么引入！甚至明明用引入了却又不见得生效！  </p>
<p>而在PhalApi，我们则是： <strong>简单、统一、规范</strong>。</p>
<h2>1.21.2 PEAR包命名规范</h2>
<p>PEAR包的类文件路径和类名映射非常简单，如下图：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_cec1c175fa70c8bc904389be30712bc3" alt="0215" /><br />
或者查看（里面有很好的代码风格编写说明）： <a href="http://www.php-fig.org/psr/psr-0/">Autoloading Standard</a>  </p>
<p>而PhalApi则更简单，我们暂时还不使用命名空间，所以namespace这一块省去。例如：  </p>
<pre><code class="language-javascript">Api_User
Domain_User
Model_User</code></pre>
<p>对应以下路径的文件：</p>
<pre><code class="language-javascript">.
|-- Api
|   `-- User.php
|-- Domain
|   `-- User.php
|-- Model
|   `-- User.php</code></pre>
<p>再举一个模拟的示例，如类Api_Game_User_Equitment对应以下文件路径：</p>
<pre><code class="language-javascript">./Api/Game/User/Equitment.php</code></pre>
<p>此外，还需注意区分大小写。  </p>
<h2>1.21.3 挂靠式自动加载</h2>
<p>然后在我们准备了这些类和文件后，我们怎么才能让这些类被框架所自动加载呢？  </p>
<p>我们提供的方式是：  <strong>挂靠式自动加载</strong> 。  </p>
<p>熟悉Linux系统的同学可能很容易明白，还没接触到Linux的同学也是可以很快理解的。这里稍微说明一下。  </p>
<p>所谓的 <strong>挂靠</strong> 就是把你代码的目录放在项目的目录下（或者其他任何一个地方），随后简单地将此目录添加到自动加载器即可。如上面的：</p>
<pre><code class="language-javascript">DI()-&gt;loader-&gt;addDirs('Demo');</code></pre>
<p>当有多个目录时，可以这样：</p>
<pre><code class="language-javascript">DI()-&gt;loader-&gt;addDirs(array('Demo', 'Demo1', 'Demo2'));</code></pre>
<p>需要注意的是，上面相对路径的都需要放置在应用项目的目录API_ROOT下面。如果是单个文件的引入，则可以使用绝对路径。下面分别简单说明。  </p>
<h3>(1)相对路径的目录路径挂靠</h3>
<p>通过PhalApi_Loader::addDirs()方式挂靠的路径，都是强制在目录API_ROOT下面，即下面的三种方式是等效的（在Linux系统上）：  </p>
<pre><code>//路径：API_ROOT/Demo
DI()-&gt;loader-&gt;addDirs('Demo');

//路径：API_ROOT/./Demo
DI()-&gt;loader-&gt;addDirs('./Demo');

//路径：API_ROOT/Demo
DI()-&gt;loader-&gt;addDirs('/Demo');</code></pre>
<p>如果需要挂靠的目录不在项目目录下，在Linux可以通过软链来解决。  </p>
<h3>(2)单个文件允许绝对路径引入</h3>
<p>对于单个文件的引入，可以通过PhalApi_Loader::loadFile()来引入。注意以下两种写法的区别：  </p>
<pre><code>//路径：API_ROOT/Demo/Tool.php
DI()-&gt;loader-&gt;loadFile('Demo/Tool.php');

//路径：/path/to/Demo/Tool.php
DI()-&gt;loader-&gt;loadFile('/path/to/Demo/Tool.php');</code></pre>
<hr />
<h4><a href="/wikis/%5b1.20%5d-DB%e6%93%8d%e4%bd%9c%ef%bc%9a%e6%95%b0%e6%8d%ae%e5%ba%93%e5%9f%ba%e6%9c%ac%e6%93%8d%e4%bd%9c%e9%80%9f%e6%9f%a5.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.22%5D-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AD%BE%E5%90%8D%E8%A7%84%E5%88%99.html">下一章</a>#[1.22]-签名验证：自定义签名规则</h4>
<h4><a href="/wikis/%5B1.21%5D-%E7%B1%BB%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD%EF%BC%9A%E9%81%B5%E5%BE%AAPEAR%E5%8C%85%E7%9A%84%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.23%5D-%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%EF%BC%9AGET%E5%92%8CPOST%E4%B8%A4%E8%80%85%E7%9A%86%E5%8F%AF%E5%BE%97%E5%8F%8A%E8%B6%85%E8%B6%8AJSON%E6%A0%BC%E5%BC%8F%E8%BF%94%E5%9B%9E.html">下一章</a></h4>
<hr />
<h2>1.22.1 过滤器服务</h2>
<p>正如我们核心思想DI里面所说的，我们把后台很多功能资源都称为服务，所以在PhalApi框架中我们已经系统规定了<strong>DI()-&gt;filter</strong>为过滤器服务，以实现接口请求时的一些拦截操作，一如现在要说明的签名验证。  </p>
<p>在接口进行初始化时，会自动调用已注册的过滤器服务 <strong>DI()-&gt;filter</strong> ，关键的代码如下：</p>
<pre><code class="language-javascript">//$vim ./PhalApi/PhalApi/Api.php
    public function init()
    {
        $this-&gt;createMemberValue();

        $this-&gt;filterCheck();

        $this-&gt;checkStatus();
    }

    protected function filterCheck()
    {
        $filter = DI()-&gt;filter;

        if (isset($filter)) {
            $filter-&gt;check();
        }
    }</code></pre>
<h3>默认直接可用的接口验证</h3>
<p>基于很多同学对接口签名验证比较陌生，从框架推出签名验证以来，很长一段时间内，很多同学对于如何实现一个接口签名依然不知如何下手。<br />
为了给大家更好的便利性，我们提供了一个基本版的接口验证服务。  </p>
<p>主要是基于md5进行的签名生成，这个只能作为一般性的参考。大家可以在此基础上进行调整延伸。  </p>
<p>默认情况下，可以去掉注释开启使用PhalApi_Filter_SimpleMD5进行接口验证，即：  </p>
<pre><code class="language-javascript">//签名验证服务
DI()-&gt;filter = 'PhalApi_Filter_SimpleMD5';</code></pre>
<p>其验签的算法如下（如注释所示）：  </p>
<pre><code class="language-javascript">1、排除签名参数（默认是sign）
2、将剩下的全部参数，按参数名字进行字典排序
3、将排序好的参数，全部用字符串拼接起来
4、进行md5运算</code></pre>
<p>以下面的示例参数为例，即：  </p>
<pre><code>1、排除签名参数（默认是sign）
?service=Default.Index&amp;username=dogstar

2、将剩下的全部参数，按参数名字进行字典排序
service=Default.Index
username=dogstar

3、将排序好的参数，全部用字符串拼接起来
"Default.Indexdogstar" = "Default.Index" + "dogstar"

4、进行md5运算
sign = 35321cc43cfc1e4008bf6f1bf9b7e3b8 = md5("Default.Indexdogstar")

5、请求时，加上签名参数
?service=Default.Index&amp;username=dogstar&amp;sign=35321cc43cfc1e4008bf6f1bf9b7e3b8</code></pre>
<p>下面是两个调用示例，错误请求下（即签名失败）：  </p>
<pre><code>http://localhost/phalapi/public/demo/?service=Default.Index&amp;username=dogstar

返回：
{
    "ret": 406,
    "data": [],
    "msg": "非法请求：签名错误"
}</code></pre>
<blockquote>
<p>温馨提示：<br />
签名错误情况下，可以查看日记获得正确的sign，如：  </p>
</blockquote>
<pre><code>2015-10-23 23:16:16|DEBUG|Wrong Sign|{"needSign":"35321cc43cfc1e4008bf6f1bf9b7e3b8"}</code></pre>
<p>正常请求下（带sign签名）：  </p>
<pre><code>http://localhost/phalapi/public/demo/?service=Default.Index&amp;username=dogstar&amp;sign=35321cc43cfc1e4008bf6f1bf9b7e3b8</code></pre>
<p>如果不想使用sign作为关键的签名参数，可以在注册时指定，如使用缩写s：  </p>
<pre><code>DI()-&gt;filter = new PhalApi_Filter_SimpleMD5('s');</code></pre>
<h2>1.22.2 微信签名示例</h2>
<p>所以，如果我们需要实现签名验证，只需要简单的两步即可：  </p>
<ul>
<li>1、实现过滤器接口 <strong>PhalApi_Filter::check()</strong>；</li>
<li>2、注册过滤器服务 <strong>DI()-&gt;filter</strong>；</li>
</ul>
<p>下面以大家熟悉的 <a href="http://mp.weixin.qq.com/wiki/17/2d4265491f12608cd170a95559800f2d.html">微信验签</a> 为例，进行示例说明。</p>
<h3>(1)实现过滤器接口 PhalApi_Filter::check()</h3>
<p>通常我们约定返回ret = 402表示验证失败，所以当签名失败时，我们可以返回ret = 402以告知客户端签名不对。根据微信的检验signature的PHP示例代码，我们可以快速实现自定义签名规则，如：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Common/SignFilter.php 
&lt;?php

class Common_SignFilter implements PhalApi_Filter
{
    public function check()
    {
        $signature = DI()-&gt;request-&gt;get('signature');
        $timestamp = DI()-&gt;request-&gt;get('timestamp');
        $nonce = DI()-&gt;request-&gt;get('nonce');  

        $token = 'Your Token Here ...';
        $tmpArr = array($token, $timestamp, $nonce);
        sort($tmpArr, SORT_STRING);
        $tmpStr = implode( $tmpArr );
        $tmpStr = sha1( $tmpStr );

        if ($tmpStr != $signature) {
            throw new PhalApi_Exception_BadRequest('wrong sign', 1);
        }
    }
}</code></pre>
<h3>(2)注册过滤器服务 DI()-&gt;filter</h3>
<p>随后，我们只需要再简单地注册一下过滤器服务即可，在init.php初始化文件最后追加：</p>
<pre><code class="language-javascript">//$ vim ./Public/init.php 
//签名验证服务
DI()-&gt;filter = 'Common_SignFilter';</code></pre>
<h3>(3)运行效果</h3>
<p>当我们再次请求接口时，如默认的服务：/demo/?service=Default.Index，即会出现以下的错误：
<img src="http://webtools.qiniudn.com/20150411005257_2bf7d70e9a3a82cae524c03e0fcc6662" alt="apic" />   </p>
<p>即：</p>
<pre><code class="language-javascript">{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}</code></pre>
<p>如果符合接口签名的验证，则会正常返回我们熟悉的内容，如：</p>
<pre><code class="language-javascript">/demo/?service=Default.Index&amp;signature=b75e0a1b574d4e111a1d6ed3c9cfbe2ccdc09404&amp;timestamp=123&amp;nonce=123</code></pre>
<p>会返回：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "title": "Default Api",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.0",
        "time": 1423055188
    },
    "msg": ""
}</code></pre>
<h2>1.22.3 特殊的场景</h2>
<h3>(1)个别接口不需要验签？</h3>
<p>在注册好统一的接口验签的过滤器拦截服务后，是会存在这样一种情况：即个别的接口不需要签名。  </p>
<p>而这种情况，我们也是有考虑到的。所以在提供了公共的功能的情况下，我们是可以快速灵活地进行定制化和扩展。  </p>
<p>当我们个别的接口不需要签名验证时，只需要简单地在接口子类里面重定义过滤器的检测即可，如在我们熟悉的默认服务器取消签名验证：</p>
<pre><code class="language-javascript">//vim ./Demo/Api/Default.php 
class Api_Default extends PhalApi_Api
{
    //....

    protected function filterCheck()
    {
    }
}</code></pre>
<h2>1.22.4 更好地建议</h2>
<p>通常关于接口签名这块，我们还需要：  </p>
<ul>
<li>
<p>1、为不同的接入方定义不同的密钥和私钥；</p>
</li>
<li>
<p>2、如果业务需要，为各个接口、各个接入方分配调用权限；</p>
</li>
<li>3、统一签名参数的规则，可以配置在./Config/app.php中的，如上面的签名需要的参数，我们可以追加统一的参数规则：
<pre><code class="language-javascript">/**
 * 应用接口层的统一参数
 */
'apiCommonRules' =&gt; array(
    'signature' =&gt; array('name' =&gt; 'signature', 'require' =&gt; true),
    'timestamp' =&gt; array('name' =&gt; 'timestamp', 'require' =&gt; true),
    'nonce' =&gt; array('name' =&gt; 'nonce', 'require' =&gt; true),
),</code></pre></li>
</ul>
<hr />
<h4><a href="/wikis/%5B1.21%5D-%E7%B1%BB%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8A%A0%E8%BD%BD%EF%BC%9A%E9%81%B5%E5%BE%AAPEAR%E5%8C%85%E7%9A%84%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.23%5D-%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%EF%BC%9AGET%E5%92%8CPOST%E4%B8%A4%E8%80%85%E7%9A%86%E5%8F%AF%E5%BE%97%E5%8F%8A%E8%B6%85%E8%B6%8AJSON%E6%A0%BC%E5%BC%8F%E8%BF%94%E5%9B%9E.html">下一章</a>#[1.23]-请求和响应：GET和POST两者皆可得及超越JSON格式返回</h4>
<h4><a href="/wikis/%5B1.22%5D-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AD%BE%E5%90%8D%E8%A7%84%E5%88%99.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.24%5D-%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E5%9C%B0%E5%8F%AF%E9%85%8D%E7%BD%AE%E5%8C%96%E7%9A%84%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.html">下一章</a></h4>
<hr />
<h2>1.23.1 请求</h2>
<p>把我们后台的API想象成一个函数，那么请求的参数就是我们的参数列表；而接口响应的数据则对应函数返回的结果。  </p>
<p>对于请求，正如前面所看到的，我们可以使用$_GET，也可以使用$_POST，也可以两者都使用，还可以在测试时自己指定模拟的请求数据包。  </p>
<p>或者，在实际项目开发中，我们还需要根据自身的需求，跟我们自己的客户端作一些约定。如通常地，我们会要求客户端 <strong>service参数必须用GET方式</strong> ，以便服务器返回500时定位接口服务位置。对此，简单的我们把$_POST['service']去掉即可，如在入口文件前面添加：</p>
<pre><code class="language-javascript">unset($_POST['service']);   //只接收GET方式的service参数</code></pre>
<h3>(1)在index.php入口处指定数据源</h3>
<p>很多时间，不同的项目对数据接收有不同的需求。如简单地，如果客户端是通过JSON后再传递的话，我们可以这样在入口处进行代码调整：</p>
<pre><code class="language-javascript">//vim ./index.php

$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : "{}";
DI()-&gt;request = new PhalApi_Request(array_merge($_GET,json_decode($HTTP_RAW_POST_DATA, true))); </code></pre>
<h3>(2)单元测试时指定数据源</h3>
<p>在进行单元测试时，我们需要模拟接口的请求动作，也需要提供接口参数。这时的参数的指定更为灵活。可通过以下代码来实现，即：  </p>
<pre><code class="language-javascript">//数据源
$data = array(...);

DI()-&gt;request = new PhalApi_Request($data);</code></pre>
<p>或者使用PhalApi封装的测试类来快速模拟调用接口：  </p>
<pre><code>    public function testIndexByRunner()
    {
        //Step 1. 构建请求URL
        $url = 'service=Default.Index&amp;username=dogstar';
        $params = array();

        //Step 2. 执行请求  
        $rs = PhalApi_Helper_TestRunner::go($url, $params);
    }</code></pre>
<h3>(3)接口数据的加密传送</h3>
<p>有时，出于安全性的考虑，项目需要对请求的接口参数进行对称加密传送。这时可以通过重载PhalApi_Request::genData()来轻松实现。<br />
假设，我们现在需要把全部的参数base64编码序列化后通过$_POST['data']来传递，则相应的解析代码如下。  </p>
<p>第一步，先定义自己的扩展请求类，在里面完成对称解析的动作：  </p>
<pre><code>&lt;?php

class Common_Request extends PhalApi_Request {

    public function genData($data) {
        if (!isset($data) || !is_array($data)) {
            $data = $_POST; //改成只接收POST
        }

        return isset($data['data']) ? base64_decode($data['data']) : array();
    }
}</code></pre>
<p>第二步，在index.php入口文件中重新注册请求类（即添加以下代码）：  </p>
<pre><code>//重新注册request
DI()-&gt;request = 'Common_Request'; </code></pre>
<p>然后，就可以轻松实现了接口参数的对称加密传送。<br />
至此，你也许已经发现：指定数据源和对称加密是可以结合来使用的。  </p>
<h2>1.23.2 响应</h2>
<p>当前默认使用JSON的格式返回，但项目需要其他返回格式也是可以的。只需要实现PhalApi_Response抽象中的formatResult($result)格式化返回结果，然后也是重新注册DI()-&gt;response服务即可。如：  </p>
<h3>(1)JSONP的返回</h3>
<p>在H5页面中，我们可能会需要用到JSONP的返回，所以这里默认提供了这种格式的支持。  </p>
<p>在入口文件中，添加：</p>
<pre><code class="language-javascript">//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()-&gt;response = new PhalApi_Response_JsonP($_GET['callback']);
}</code></pre>
<p>在接口访问时再带上&amp;callback=XXX参数即可。  </p>
<p>这里在创建响应服务时，可以看到是用了回调函数的名字进行初始化。考虑到会存在XSS（跨站脚本攻击），对回调函数要进行相应的过滤，可以用黑名单或者白名单的方式。黑名单方式暂时还没提供，白名单相对简单但需要项目自己实现，如：</p>
<pre><code class="language-javascript">class Common_JsonP extends PhalApi_Response_JsonP {

    protected function clearXss($callback) {
        return in_array($callback, array('fun1', 'func2', 'func3')) ? $callback : '';
    }
}</code></pre>
<h3>(2)其他格式返回</h3>
<p>如上面所述，当需要返回一种当前PhalApi没提供的格式时，可以：</p>
<ul>
<li>1、实现PhalApi_Response抽象中的formatResult($result)格式化返回结果</li>
<li>2、重新注册DI()-&gt;response服务  </li>
</ul>
<p>这里以扩展XML返回格式为例，简单说明。  </p>
<p>首先，添加实现一个XML响应子类：</p>
<pre><code class="language-javascript">&lt;?php

class Common_Response_XML extends PhalApi_Response {

    protected function formatResult($result) {
        return ArrayToXML::toXml($result);
    }
}</code></pre>
<p>关于ArrayToXML，请查看： <a href="http://www.oschina.net/code/snippet_54100_1548">将PHP数组转成XML</a>  </p>
<p>然后，重新注册：</p>
<pre><code class="language-javascript">DI()-&gt;response = new Common_Response_XML();</code></pre>
<p>done!</p>
<hr />
<h4><a href="/wikis/%5B1.22%5D-%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81%EF%BC%9A%E8%87%AA%E5%AE%9A%E4%B9%89%E7%AD%BE%E5%90%8D%E8%A7%84%E5%88%99.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.24%5D-%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E5%9C%B0%E5%8F%AF%E9%85%8D%E7%BD%AE%E5%8C%96%E7%9A%84%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.html">下一章</a>#[1.24]-缓存策略：更灵活地可配置化的多级缓存</h4>
<h4><a href="/wikis/%5B1.23%5D-%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%EF%BC%9AGET%E5%92%8CPOST%E4%B8%A4%E8%80%85%E7%9A%86%E5%8F%AF%E5%BE%97%E5%8F%8A%E8%B6%85%E8%B6%8AJSON%E6%A0%BC%E5%BC%8F%E8%BF%94%E5%9B%9E.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.25%5D-%E5%9B%BD%E9%99%85%E5%8C%96%E7%BF%BB%E8%AF%91%EF%BC%9A%E4%B8%BA%E8%B5%B0%E5%90%91%E5%9B%BD%E9%99%85%E5%8C%96%E6%8F%90%E5%89%8D%E5%81%9A%E5%A5%BD%E7%BF%BB%E8%AF%91%E5%87%86%E5%A4%87.html">下一章</a></h4>
<hr />
<h2>1.24.1 论缓存的重要性</h2>
<p>在很多情况下，我们都需要缓存。如对重复获取但变化不大的数据进行缓存以提供服务器的响应能力，又如当需要为数据库服务器减少QPS时等等。但同时缓存也因为有延时导致数据不能实时更新，或者在需要更新时不能被更新。如在接口调试、单元测试或者预览时，但这些都可以通过一些技巧来获得。这里，将从简单的缓存、再到高速缓存、多级缓存逐步进行说明。</p>
<h2>1.24.2 简单缓存</h2>
<p>这里所指的简单缓存，是指文件缓存。因为实现简单，且部署方便。但其缺点也是明显的，如文件I/O读写导致性能低，不能支持分布式。所以在没有集群服务器下是适用的。</p>
<p>当需要使用文件缓存时，先对文件缓存进行DI注册，便可使用。</p>
<pre><code class="language-javascript">//$ vim ./Public/init.php 
DI()-&gt;cache = new PhalApi_Cache_File(array('path' =&gt; API_ROOT . '/Runtime', 'prefix' =&gt; 'demo'));

// 设置
DI()-&gt;cache-&gt;set('thisYear', 2015, 600);

// 获取
echo DI()-&gt;cache-&gt;get('thisYear');

// 删除
DI()-&gt;cache-&gt;delete('thisYear');</code></pre>
<h2>1.24.3 高速缓存</h2>
<p>高速缓存通常具备分布式的能力，并能进驻内存，因此性能高。但同样此优点所带来的则是需要另外部署缓存扩展，如常见的Memcached、Redis。若需要考虑缓存落地，还要进一点筛选和配置。  </p>
<p>如使用Memcached：</p>
<pre><code class="language-javascript">DI()-&gt;cache = new PhalApi_Cache_Memcached(array('host' =&gt; '127.0.0.1', 'port' =&gt; 11211, 'prefix' =&gt; 'demo_'));

// 设置
DI()-&gt;cache-&gt;set('thisYear', 2015, 600);

// 获取
echo DI()-&gt;cache-&gt;get('thisYear');

// 删除
DI()-&gt;cache-&gt;delete('thisYear');</code></pre>
<p>从上面示例代码注意到，我们使用了前缀配置，以防同一台MC服务器同一端口下key名冲突。当然，更好是使用配置文件来配置缓存的地址和端口，以及前缀。</p>
<h2>1.24.4 多级缓存</h2>
<p>然而，很多时候，我们需要多级缓存来承载更大的访问量。以便结合本地低速缓存和分布式高速缓存来获得更好的用户体验和服务器吞吐率。这时，则可以使用多级缓存策略。  </p>
<p>在切换到多级缓存时，我们明显希望原有的代码调用不需要同步调整便能获得升级后的多级缓存，并且我们后台开发人员也希望可以轻便在原有的单点缓存的基础上进行扩展。所以这就引出了一个有趣的问题：我们该如何组织多级缓存？  </p>
<p>作为一个框架，除了考虑上述的原有调用、单点缓存复用外，还需要考虑到多级缓存的组装。部分框架，一如我最喜欢的Phalcon则是使用了配置的形式来实现。但仍然，我们需要了解其配置的各个格式要求才能更好掌握和使用，这点带来了学习的成本。因此，PhalApi则是使用了简单的组合模式来实现。  </p>
<p>正如你在源代码中看到的PhalApi_Cache_Multi类，通过此类的实例可以利用PhalApi_Cache_Multi::addCache()快速添加一个缓存节点，而节点的优先级则按开发同学添加的顺序来确定。显然在添加时，我们应该先添加分布式的高速缓存，再添加本地的低速缓存（希望不会有人先添加文件缓存再添加MC缓存）。而各个节点的初始化，则是我们之前所熟悉的。只是简单添加即可。</p>
<p>以下是结合文件缓存和MC缓存的多级缓存示例：</p>
<pre><code class="language-javascript">$cache = PhalApi_Cache_Multi();

$mcCache = new PhalApi_Cache_Memcached(array('host' =&gt; '127.0.0.1', 'port' =&gt; 11211, 'prefix' =&gt; 'demo_'));
$cache-&gt;addCache($mcCache);

$fileCache = new PhalApi_Cache_File(array('path' =&gt; API_ROOT . '/Runtime', 'prefix' =&gt; 'demo'));
$cache-&gt;addCache($fileCache);

DI()-&gt;cache = $cache;</code></pre>
<p>然后，就可像之前那样设置、获取和删除缓存，而不需考虑是单点缓存，还是多级缓存：</p>
<pre><code class="language-javascript">// 设置
DI()-&gt;cache-&gt;set('thisYear', 2015, 600);

// 获取
echo DI()-&gt;cache-&gt;get('thisYear');

// 删除
DI()-&gt;cache-&gt;delete('thisYear');</code></pre>
<h2>1.24.5 静态结构图</h2>
<p>对应地，我们可以得出清晰明了的UML静态结构图：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_51bddc79686ee2215d9b0f1144d903bf" alt="多级缓存静态结构图" /></p>
<p>结构层次非常简单，但主要分为三大类：左边是多级缓存；中间突出的是特殊情况，即：空对象模式下的空缓存；右边是目前已提供或者后期扩展的具体缓存实现。</p>
<hr />
<h4><a href="/wikis/%5B1.23%5D-%E8%AF%B7%E6%B1%82%E5%92%8C%E5%93%8D%E5%BA%94%EF%BC%9AGET%E5%92%8CPOST%E4%B8%A4%E8%80%85%E7%9A%86%E5%8F%AF%E5%BE%97%E5%8F%8A%E8%B6%85%E8%B6%8AJSON%E6%A0%BC%E5%BC%8F%E8%BF%94%E5%9B%9E.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.25%5D-%E5%9B%BD%E9%99%85%E5%8C%96%E7%BF%BB%E8%AF%91%EF%BC%9A%E4%B8%BA%E8%B5%B0%E5%90%91%E5%9B%BD%E9%99%85%E5%8C%96%E6%8F%90%E5%89%8D%E5%81%9A%E5%A5%BD%E7%BF%BB%E8%AF%91%E5%87%86%E5%A4%87.html">下一章</a>#[1.25]-国际化翻译：为走向国际化提前做好翻译准备</h4>
<h4><a href="/wikis/%5B1.24%5D-%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E5%9C%B0%E5%8F%AF%E9%85%8D%E7%BD%AE%E5%8C%96%E7%9A%84%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.26%5D-%E6%95%B0%E6%8D%AE%E5%AE%89%E5%85%A8%EF%BC%9A%E6%95%B0%E6%8D%AE%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E6%96%B9%E6%A1%88.html">下一章</a></h4>
<hr />
<p><em>Your new understanding has made you powerful. Please use your new powers for good.</em>    </p>
<h2>1.25.1 固定的中文写法</h2>
<p>一直以来，我们都是硬编码方式返回中文的文案或者提示，如：</p>
<pre><code class="language-javascript">$rs['msg'] = '用户不存在';</code></pre>
<p>这种写法在项目根本不需要考虑国际化翻译时，是完全没问题的。</p>
<h2>1.25.2 通用的翻译写法</h2>
<p>当我们需要进行翻译时，可以这样进行调整：</p>
<pre><code class="language-javascript">$rs['msg'] = T('user not exists');</code></pre>
<p>然后在对应的翻译文件中（如中文对应文件是：./Language/zh_cn/common.php）添加对应的翻译即可：</p>
<pre><code class="language-javascript">// $vim ./Language/zh_cn/common.php
return array(
    'user not exists' =&gt; '用户不存在',
);</code></pre>
<h3>(1)当存在动态变量时？</h3>
<p>有时，我们需要动态返回一些值，这里可以用 <strong>大括号</strong> 将需要动态替换的值包住并提供替换的参数即可，一如：</p>
<pre><code class="language-javascript">echo T('hello {name}', array('name' =&gt; 'dogstar'));  </code></pre>
<p>如果对应的中文翻译是：</p>
<pre><code class="language-javascript">'hello {name}' =&gt; '您好，{name}',</code></pre>
<p>将会看到输出：</p>
<pre><code class="language-javascript">您好，dogstar</code></pre>
<h3>(2)当多个动态变量时，还可以这样省略变量名</h3>
<p>如果要简化动态变量的传递，可以这样：  </p>
<pre><code class="language-javascript">echo T('{0} I love you because {1}', array('PhalApi', 'no reasons'));</code></pre>
<p>如果对应的中文翻译是：</p>
<pre><code class="language-javascript">'{0} I love you because {1}' =&gt; '{0} 我爱你因为{1}',</code></pre>
<p>将会看到输出：</p>
<pre><code class="language-javascript">PhalApi 我爱你因为no reasons</code></pre>
<h3>(3)当翻译不存在时？</h3>
<p>翻译不存在，有两种场景：一是指定的语言包不存在；二是语言包存在但翻译不存在。无论何种情况，找不到翻译时，都会用代码硬编码的内容返回。</p>
<h2>1.25.3 语言的设定</h2>
<p>当我们拥有了多种语言时，则可以在入口/初始化文件中，选择设定需要的语言。</p>
<pre><code class="language-javascript">SL('zh_cn');    //SL()函数等效于PhalApi_Translator::setLanguage()</code></pre>
<p>参数即为语言包的路径名，如下面的en, zh_cn：  </p>
<pre><code class="language-javascript">.
|-- en
|   `-- common.php
`-- zh_cn
    `-- common.php</code></pre>
<p>此处，也可以通过客户端传递参数来自行选择语言。简单地：</p>
<pre><code class="language-javascript">SL($_GET['lan']);</code></pre>
<h2>1.25.4 添加语言翻译包</h2>
<p>有时需要手动添加语言翻译包，因为对应的翻译不一定在项目的./Language下，如扩展类库。  </p>
<p>当需要添加额外的语言翻译包时，可以这样添加：  </p>
<pre><code>        PhalApi_Translator::addMessage(API_ROOT . '/Library/User');</code></pre>
<p>在User这个目录下的翻译包类似地，为：  </p>
<pre><code>./Language/
└── zh_cn
    └── common.php

1 directory, 1 file</code></pre>
<p>相当于追加一个额外的项目路径，其他的加载过程和原来的一样。  </p>
<h2>1.25.5 建议</h2>
<p>所以，你可以轻松看到，所谓的翻译也只是通过数组下标找一下对应的内容而已，没有太多的技术性，也没有过多的性能问题。  </p>
<p>但正是有这样提前周到的国际化准备，我们可以对外（如像产品、BOSS和外界）传送这样一个隐喻： <strong>我们的项目可以快速支持国际翻译</strong> 。<br />
这听起来多么高大尚啊！因为那些不懂技术的人，根本不在乎是用PHP的数组来存放还是什么技术，而在于能不能走向国际化。  </p>
<p>SO？既然翻译”无伤大雅“（指对性能的影响和对代码编写的阻碍），统一使用翻译的写法是值得推荐的。  </p>
<p>即使项目没有机会用到真正的翻译，但至少有两点我认为也是有用的：</p>
<ul>
<li>1、便于产品维护接口返回的提示文案；</li>
<li>2、被同行问到时，你们有支持i18n吗？我们也可以笑着回答：有 ^_^ ；</li>
</ul>
<hr />
<h4><a href="/wikis/%5B1.24%5D-%E7%BC%93%E5%AD%98%E7%AD%96%E7%95%A5%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E5%9C%B0%E5%8F%AF%E9%85%8D%E7%BD%AE%E5%8C%96%E7%9A%84%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.26%5D-%E6%95%B0%E6%8D%AE%E5%AE%89%E5%85%A8%EF%BC%9A%E6%95%B0%E6%8D%AE%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86%E6%96%B9%E6%A1%88.html">下一章</a>#[1.26]-数据安全：数据对称加密方案</h4>
<h4><a href="/wikis/%5B1.25%5D-%E5%9B%BD%E9%99%85%E5%8C%96%E7%BF%BB%E8%AF%91%EF%BC%9A%E4%B8%BA%E8%B5%B0%E5%90%91%E5%9B%BD%E9%99%85%E5%8C%96%E6%8F%90%E5%89%8D%E5%81%9A%E5%A5%BD%E7%BF%BB%E8%AF%91%E5%87%86%E5%A4%87.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.27%5D-%E7%B2%BE%E7%9B%8A%E5%BC%80%E5%8F%91%EF%BC%9A%E6%9B%B4%E5%AF%8C%E8%A1%A8%E7%8E%B0%E5%8A%9B%E7%9A%84Model%E5%B1%82%E5%92%8C%E9%87%8D%E9%87%8F%E7%BA%A7%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E7%9A%84%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88.html">下一章</a></h4>
<hr />
<h2>1.26.1 对称加密和非对称加密</h2>
<p>关于数据加密和安全这一块，只作简单说明。  </p>
<p>首先，对称加密是指数据可以加密成密文也可以解密还原，共用同一个密钥，而非对称是则公钥和私钥，两者皆使用可逆加密算法。不可逆加密不存在密钥，只有salt，用来增加可变性和随机性，如md5。而在加密里面，又数块算法加密的方式最为完善，随附图一张：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_f66cbee3d910a092bfee33dd72a2f3cd" alt="05152" /></p>
<h2>1.26.2 PHP的mcrypt加密扩展</h2>
<p>在PhalApi中，同样也是使用了mcrypt作为底层的数据加密技术方案。请查看： <a href="http://php.net/manual/zh/book.mcrypt.php">PHP 手册 函数参考 加密扩展</a>  </p>
<h2>1.26.3 加解密的使用</h2>
<p>在单元测试中，我们可以快速找到加密和解密的使用，这里再简单举一例：</p>
<pre><code class="language-javascript">$mcrypt = new PhalApi_Crypt_Mcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt-&gt;encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt-&gt;decrypt($encryptData, $key);
var_dump($decryptData);</code></pre>
<p>上面将会输出(有乱码)：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_f8e1f72b08a9520c391295ca428a9ac5" alt="0215" /></p>
<h2>1.26.4 更富弹性和便于存储的加密方案</h2>
<p>上面看到，mcrypt下的加密在两点不足：</p>
<ul>
<li>1、有乱码，不能很好地永久化存储；</li>
<li>2、只针对文本字符串的加密，不支持数组等，且无法还原类型；</li>
</ul>
<p>为此， 我们提供了更富弹性和便于存储的加密方案，即：序列化 + base64 + mcrypt的多重加密方案。  </p>
<p>以下是上面的示例-多重加密版：</p>
<pre><code class="language-javascript">$mcrypt = new PhalApi_Crypt_MultiMcrypt('12345678');

$data = 'The Best Day of My Life';
$key = 'phalapi';

$encryptData = $mcrypt-&gt;encrypt($data, $key);
var_dump($encryptData);

$decryptData = $mcrypt-&gt;decrypt($encryptData, $key);
var_dump($decryptData);</code></pre>
<p>对应的输出（这里使用了文字结果输出，是因为没了乱码）：</p>
<pre><code class="language-javascript">string(44) "rmFMdhvszAkHhOdzwt/APBACk/Mn/SqhV1Ahp1xT0Gk="
string(23) "The Best Day of My Life"</code></pre>
<h2>1.26.5 RSA的支持与超长字符串的应对方案</h2>
<p>基于项目有使用RSA进行加密和解密的需求，这里特扩展对RSA的支持。同时针对到RSA对字符串长度的限制，提供了分段处理的方案。RSA加密模块的静态类结构UML如下：<br />
<img src="http://webtools.qiniudn.com/20150411005257_e38fc8af28ac9c382e0e3ef8efbb2b86" alt="rsa-PhalApi" /></p>
<h3>(1)原生态的通信加密和解密</h3>
<p>此部分只是简单地封装了openssl相关函数的操作，可以实现与其他语言和客户端下RSA的加密通信。<br />
唯一需要注意的是，对于 <strong>“私钥加密，公钥解密”</strong> 和 <strong>“公钥加密，私钥解密”</strong> 这两种情况下key的互换和对应问题。不要混淆。  </p>
<h3>(2)超长字符串的分段处理</h3>
<p>这里重点说明一下超长字符串通信加密的问题。<br />
解决方案主要涉及两点：一是分段的处理，二是中间层转换。分段是指将待加密的字符串分割成允许最大长度117（有用户反馈说是127）内的数组，再各自处理；中间层转换是为了稳定性、通用性和方便落地存储，使用了json和base64的结合编码。  </p>
<p>虽然此方案解决了超长字符串的问题，但需要特别指出的是， <strong>不能与其他语言、或者PHP其他框架和客户端进行原生态的RSA通信</strong> 。<br />
我们突破了长度的限制，但失去了通用性。这里罗列一下各个场景和对应的处理方式：</p>
<h4>支持：PhalApi项目A  &lt;--&gt; PhalApi项目A</h4>
<h4>支持：PhalApi项目A  &lt;--&gt; PhalApi项目B，PhalApi项目C，PhalApi项目D，...</h4>
<h4>不支持：PhalApi项目 &lt;--&gt; 非PhalApi项目的PHP项目</h4>
<p>解决方案：将以下RSA模块相关的代码以包的形式拷贝到需要的PHP项目。  </p>
<pre><code class="language-javascript">.
├── Crypt
│   └── RSA
│       ├── KeyGenerator.php
│       ├── MultiBase.php
│       ├── MultiPri2Pub.php
│       ├── MultiPub2Pri.php
│       ├── Pri2Pub.php
│       └── Pub2Pri.php
└── Crypt.php
</code></pre>
<h4>不支持：PhalApi项目 &lt;--&gt; 非PHP语言的项目</h4>
<p>解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。  </p>
<h4>不支持：PhalApi项目 &lt;--&gt; 客户端（iOS/Android/Windows Phone, etc）</h4>
<p>解决方案：参考PhalApi对RSA超长字符串的处理，同步实现。  </p>
<h3>(3)使用示例：2块2毛2和故事</h3>
<p>这是一个有趣的故事，发生在我一个很好的朋友身上。   </p>
<p>在去年暑假的时候，有位师妹通过几层关系找到我朋友，想他帮她支付回家的火车票。本着“调试”（调戏）的精神，我朋友爽快地答应了并让她把支付宝帐号发过来。然后，不一会，我朋友收到了一条有支付宝帐号的短信，立马打了2.22元过去，并附言： <strong>别问我是谁，请叫我雷锋！</strong> 后来证实师妹没发来过短信，原来收到的那条是诈骗的短信。。。。你能想象得到骗子收到这2.22元和看到这条留言时的表情么，哈哈~~      </p>
<p>以下测试代码，以上面故事为背景，并演示了RSA的使用示例：</p>
<pre><code class="language-javascript">    public function testDecryptAfterEncrypt()
    {
        $keyG = new PhalApi_Crypt_RSA_KeyGenerator();
        $privkey = $keyG-&gt;getPriKey();
        $pubkey = $keyG-&gt;getPubKey();

        DI()-&gt;crypt = new PhalApi_Crypt_RSA_MultiPri2Pub();

        $data = 'AHA! I have $2.22 dollars!';

        $encryptData = DI()-&gt;crypt-&gt;encrypt($data, $privkey);

        $decryptData = DI()-&gt;crypt-&gt;decrypt($encryptData, $pubkey);

        $this-&gt;assertEquals($data, $decryptData);
    }</code></pre>
<h2>1.26.6 建议</h2>
<p>在上面的加密中，接口项目在开发时，需要自定义两个值：加密向量和私钥。  </p>
<p>为了提高数据加密的安全度，建议：</p>
<ul>
<li>加密向量项目统一在./Config/app.php中配置；</li>
<li>各模块业务数据加密所用的Key则由各业务点自定义；</li>
</ul>
<p>这样，可以对不同的数据使用不同的加密私钥，即使破解了某一个用户的数据，也难以破解其他用户的。</p>
<h2>1.26.7 扩展你的对称加密</h2>
<p>尤其对于加密方案和算法，我们在项目开发决策时，更应该优先考虑使用现在行业内成熟公认的加密方案和算法，而不是自己去从头研发。  </p>
<p>但如果你项目确实有此需要，或者需要在mcrypt的基础上再作一些变通，也是可以很快地实现和注册使用。  </p>
<p>首先，请先实现下面的加密接口：</p>
<pre><code class="language-javascript">// $vim ./PhalApi/PhalApi/Crypt.php

interface PhalApi_Crypt {

    public function encrypt($data, $key);

    public function decrypt($data, $key);
}</code></pre>
<p>然后，重新注册加密服务即可。</p>
<hr />
<h4><a href="/wikis/%5B1.25%5D-%E5%9B%BD%E9%99%85%E5%8C%96%E7%BF%BB%E8%AF%91%EF%BC%9A%E4%B8%BA%E8%B5%B0%E5%90%91%E5%9B%BD%E9%99%85%E5%8C%96%E6%8F%90%E5%89%8D%E5%81%9A%E5%A5%BD%E7%BF%BB%E8%AF%91%E5%87%86%E5%A4%87.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.27%5D-%E7%B2%BE%E7%9B%8A%E5%BC%80%E5%8F%91%EF%BC%9A%E6%9B%B4%E5%AF%8C%E8%A1%A8%E7%8E%B0%E5%8A%9B%E7%9A%84Model%E5%B1%82%E5%92%8C%E9%87%8D%E9%87%8F%E7%BA%A7%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E7%9A%84%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88.html">下一章</a>#[1.27]-精益开发：更富表现力的Model层和重量级数据获取的应对方案</h4>
<p><em>我们能够有意识地推断我们想要在哪一条想法的溪流中遨游，然而此后与那些想法的接触会潜在地塑造我们的习惯和信仰。 -- 《智慧社会》</em>  </p>
<h2>1.27.1 写在前面的话</h2>
<p>此篇章有点长，但我认为是值得一读的。因为这里我将逐步讲述如何在已有的基础上演变扩展出更高层次的代码结构和系统架构，而不致于因目前频繁的需求变更而导致代码凌乱不堪。更为重要的是，你将能从中发现，如何在一个框架中持续演变，最终体验浮现式设计的乐趣。如果你的项目亦能如此，我相信你会找到编程如同搭建积木般轻便明了的感觉。</p>
<h2>1.27.2 更富表现力的Model层</h2>
<p>接口不尽相同，主要区别在于领域业务数据的处理。而数据的来源则更为广泛，可能是来自数据库，可能来自第三方平台接口，可能存放于内存。所以，PhalApi这里的Model层，则是 <strong>广义上的数据源层</strong> ，用于获取原始的业务数据，而不管来自何方，何种存储媒介。这也是为什么我们没有将Model层打造成活动纪录或者数据映射器的原因。当然，如果你确实需要，也可以自行调整。  </p>
<p>如果数据来源于数据库，我们则需要考虑到数据库服务器的感受，保证不会有过载的请求而导致它罢工。对此，我们可以结合缓存来进行性能优化。</p>
<p>如，一般地：</p>
<pre><code class="language-javascript">
    // 版本1：简单的获取
        $model = new Model_User();
        $rs = $model-&gt;getByUserId($userId);</code></pre>
<p>这种是没有缓存的情况，当发现有性能问题并且可以通过缓存来解决时，我们可以在调用时简单引入缓存：</p>
<pre><code class="language-javascript">        // 版本2：使用单点缓存/多级缓存 (应该移至Model层中)
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()-&gt;cache-&gt;get($key);
        if ($rs === NULL) {
                $rs = $model-&gt;getByUserId($userId);
                DI()-&gt;cache-&gt;set($key, $rs, 600);
        }</code></pre>
<p>但不建议在领域Domain层中引入缓存，因为会导致混淆和不便进行测试。更好是将缓存的处理移至Model，保持数据获取的透明性：</p>
<pre><code class="language-javascript">class Model_User extends PhalApi_Model_NotORM {

    public function getByUserIdWithCache($userId) {
        $key = 'userbaseinfo_' . $userId;
        $rs = DI()-&gt;cache-&gt;get($key);
        if ($rs === NULL) {
            $rs = $this-&gt;getByUserId($userId);
            DI()-&gt;cache-&gt;set($key, $rs, 600);
        }
            return $rs;
    }</code></pre>
<p>对应地，外部的调用调整成：</p>
<pre><code class="language-javascript">    // 版本2：使用单点缓存/多级缓存 (应该移至Model层中)
        $model = new Model_User();
        $rs = $model-&gt;getByUserIdWithCache($userId);</code></pre>
<p>至此，Model层对于上层如Domain来说，负责获取源数据，而不管此数据来自于数据库，还是远程接口，抑或是缓存包装下的数据。这正是我们使用数组在Model层和Domain层通讯的原因，因为数组更加通用，不需要额外添加实体。</p>
<h2>1.27.3 重量级数据获取的应对方案</h2>
<p>纵使更富表现力的Model很好地封装了源数据的获取，但是仍然会遇到一些尴尬的问题。特别地，当我们大量地进行缓存读取判断时，会出现很多重复的代码，这样既不雅观也难以管理，甚至会出现一些简单的人为编写错误而导致的BUG。另外，当我们需要进行预览、调试或测试时，我们是不希望看到缓存的，即我们能够手工指定是否需要缓存。  </p>
<p>这里再稍微简单回顾总结一下我们现在的问题：我们希望通过缓存策略来优化Model层的源数据获取，特别当源数据获取的成本非常大时。但我们又希望我们可以轻易控制何时需要缓存，何时不需要，并且希望原有的代码能在OCP的原则下不需要修改，但又能很好地传递源数据获取的复杂参数。归纳一下，则可分为三点：缓存的控制、源数据的获取、复杂参数的传递。  </p>
<h3>(1)缓存的控制</h3>
<p>不管是单点缓存，还是多级缓存，都希望使用原有已经注册的cache组件服务。所以，应该使用委托。委托的另一个好处在于使用外部依赖注入可以获得更好的测试性。  </p>
<h3>(2)源数据的获取</h3>
<p>源数据的获取，作为源数据获取的主要过程和主要实现，需要进行缓存的控制（可细分为：是否允许读缓存、和是否允许写缓存）、  获取缓存的key值和有效时间，以及最终原始数据的获取。明显，这里应该使用模板方法，然后提供钩子函数给具体子类。  </p>
<p>这里，我们提供了Model代理抽象类PhalApi_ModelProxy。  </p>
<p>之所以使用代理模式，是因为实际上并不一定会真正调用到最终源数据的获取，因为往往源数据的获取成本非常高，故而我们希望通过缓存来拦截数据的获取。  </p>
<p>由于Model代理被上层的Domain领域层调用，但又依赖于下层Model层获得原始数据，所以处于Domain和Model之间。为了保持良好的项目代码层级，如果需要创建PhalApi_ModelProxy子类，建议新建一个ModelProxy目录。  </p>
<p>如对用户基本信息的获取，我们添加了一个代理：</p>
<pre><code class="language-javascript">class ModelProxy_UserBaseInfo extends PhalApi_ModelProxy {

    protected function doGetData($query) {
        $model = new Model_User();

        return $model-&gt;getByUserId($query-&gt;id);
    }

    protected function getKey($query) {
        return 'userbaseinfo_' . $query-&gt;id;
    }

    protected function getExpire($query) {
        return 600;
    }
}</code></pre>
<p>其中，doGetData($query)方法由具体子类实现，委托给Model_User的实例进行源数据获取。另外，实现钩子函数以返回缓存唯一key，和缓存的有效时间。  </p>
<p>这里只是作为简单的示例，更好的建议是应该将缓存的时间纳入配置中管理，如 <strong>配置四个缓存级别：低(5 min)、中(10 min)、高(30 min)、超(1 h)</strong> ，然后根据不同的业务数据使用不同的缓存级别。这样，即便于团队交流，也便于缓存时间的统一调整。</p>
<h3>(3)复杂参数的传递</h3>
<p>敏锐的读者会发现，上面有一个$query查询对象，这就是我们即将谈到的复杂参数的传递。  </p>
<p>$query是查询对象PhalApi_ModelQuery的实例。我们强烈建议此类实例应当被作为 <strong>值对象</strong> 对待。虽然我们出于便利将此类对象设计成了结构化的使用。但你可以轻松通过new PhalApi_ModelQuery($query-&gt;toArray())来拷贝一个新的查询对象。  </p>
<p>此查询对象，目前包括了四个成员变量：是否读缓存、 是否写缓存、主键id、时间戳。  </p>
<p>很多时候，这四个基本的变量是满足不了各项目的实际需求的，因此你可以定义你的查询子类， 以支持丰富的数据获取。如调用优酷平台接口获取用户最近上传发布的视频时，需要用户昵称、获取的数量、排序种类等。</p>
<h3>(4)最终的调用</h3>
<p>在完成了上面的工作后，让我们看下最终呈现的效果：</p>
<pre><code class="language-javascript">        // 版本3：缓存 + 代理
        $query = new PhalApi_ModelQuery();
        $query-&gt;id = $userId;
        $modelProxy = new ModelProxy_UserBaseInfo();
        $rs = $modelProxy-&gt;getData($query);</code></pre>
<p>在领域层中，我们切换到了Model代理获取数据，而不再是原来的Model直接获取。其中新增的是代理具体类 ModelProxy_UserBaseInfo，和可选的查询类。  </p>
<h3>(5)UML静态图</h3>
<p>至此，我们很好地在源数据的获取基础上，统一结合缓存策略。你会发现： <strong>缓存节点可变、具体的源数据可变、复杂的查询亦可变</strong> 。  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_2d0722fc5d36d179cb6e07885591f737" alt="重量级数据获取的应对方案" />  </p>
<p>将此图简化一下，可得到：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_e11ba65eef6f2590aaa0ed74ddc7d3d0" alt="重量级数据获取的应对方案-small" />  </p>
<p>这样的设计是合理的，因为缓存节点我们希望能在项目内共享，而不管是哪块的业务数据；对于具体的源数据获取明显也是不尽相同，所以也需要各自实现，同时对于同一类业务数据（如用户基本信息）则使用一样的缓存有效时间和指定格式的缓存key（通常结合不同的id组成唯一key）；最后在前面的缓存共享和同类数据的基础上，还需要支持不同数据的具体获取，因此需要查询对象。也就是说，你可以在不同的层级不同的范畴内进行自由的控制和定制。    </p>
<p>如果退回到最初的版本，我们可以对比发现，Model Proxy就是Domain和Model间的桥梁，即：中间层。因为每次直接通过Model获取源数据的成本较大，我们可以通过Model Proxy模型代理来缓存获取的数据来减轻服务器的压力。  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_a78ada838cb1682958d9b16f490d3f92" alt="重量级数据获取的应对方案-first" /></p>
<h2>1.27.4 细粒度和可测试性</h2>
<p>这无疑是细粒度的划分，但对于支撑复杂的领域业务却发挥着重要的作用。一来是如此清楚明了，二来则是带来了可测试性。  </p>
<p>正如前面提及到的，我们在预览、调试、单元测试或者后台计划任务时，不希望有缓存的干扰。在细粒度划分的基础上，可轻松用以下方法实现而不必担心会破坏代码的简洁性。  </p>
<h3>(1)取消缓存的方法1： 外部注入模拟缓存</h3>
<p>在构造Model代理时，默认情况下使用了DI()-&gt;cache作为缓存，当需要进行单元测试时，我们可以两种途径在外部注入模拟的缓存而达到测试的目的：替换全局的DI()-&gt;cache，或单次构造注入。对于计划任务则可以在统一的后台任务启动文件将DI()-&gt;cache设置成空对象。</p>
<h3>(2)取消缓存的方法2： 查询中的缓存控制</h3>
<p>在项目层次，我们可以统一构造自己的查询基类，以实现对缓存的控制。  </p>
<p>如：</p>
<pre><code class="language-javascript">class Common_ModelQuery extends PhalApi_ModelQuery {

    public function __construct($queryArr = array()) {
        parent::__construct($queryArr);

        if (DI()-&gt;debug) {
            $this-&gt;readCache = FALSE;
            $this-&gt;writeCache = FALSE;
        }
    }
}</code></pre>
<p>至于DI()-&gt;debug的设置，则可以在入口文件中根据约定的接口参数设定，简单地如：</p>
<pre><code class="language-javascript">if (isset($_GET['debug']) &amp;&amp; $_GET['debug'] == 1) {
    DI()-&gt;debug = true;
}</code></pre>
<p>这样便可以获得了接口预览和调试的能力。</p>
<h2>1.27.5 何时使用此方案？</h2>
<p>可以看到，此方案是在缓存策略（包括单点缓存、低高速缓存、多级缓存）和广义Model层基础上扩展的，以便应对重量级的业务数据获取。此方案有一定的优势，但作为代价则是额外的代码编写以及层级复杂性。并且，我们还没谈及到数据变更时的处理。    </p>
<p>所以，请在确切需要统一封装高成本的数据获取时，才使用此方案。</p>
<h2>1.27.6 扩展：多接口参数传递的优雅处理方案</h2>
<p>当接口的查询参数过多时，我们需要手工重复地将接口参数从Api层传递到Domain层，再通过Query对象传递到Model层，这中间任何一个环节的缺失或遗漏都会造成一个BUG。  </p>
<p>为此，项目可以考虑使用一种更为优雅的方案来进行整合，并实现自动化参数获取，但又保留接口原来的参数验证。  </p>
<p>假设，我们需要以下多个接口参数：  </p>
<pre><code>function getRules() {
    return array(
        'getList' =&gt; array(
            'keyword' =&gt; array(...),
            'filed' =&gt; array(...),
            'page' =&gt; array(...),
            'perpage' =&gt; array(...),
            'order' =&gt; array(...),
        ),
    );
} </code></pre>
<p>为避免出现以下这样的手工调用（而且也不符合值对象的特征）：  </p>
<pre><code>$query = new Query_Demo();
$query-&gt;keyword = $this-&gt;keyword;
$query-&gt;filed = $this-&gt;filed;
$query-&gt;page = $this-&gt;page;
$query-&gt;perpage = $this-&gt;perpage;
$query-&gt;order = $this-&gt;order;

$domain = new Domain_Demo();
$list = $domain-&gt;getList($query);</code></pre>
<p>我们首先需要提取出一个层超类：  </p>
<pre><code>class Query_Demo extends PhalApi_ModelQuery {
    public $keyWord;
    public $filed;
    public $page;
    public $perpage;
    public $order;

    public function __construct($api) {
        //按需获取，自动初始化
        $vars = get_object_vars($api);
        foreach ($vars as $key =&gt; $var) {
            if (isset($api-&gt;$key)) {
                $this-&gt;$key = $api-&gt;$key;
            }
        }
    }
}</code></pre>
<p>然后，在接口Api中对Domain层的调用就会简化成：  </p>
<pre><code>$query = new Query_Demo($this); //自动初始化

$domain = new Domain_Demo();
$list = $domain-&gt;getList($query); //通过查询对象传递众多参数</code></pre>
<p>这样的好处在于：</p>
<ul>
<li>1、更方便职能的划分</li>
<li>2、易于测试</li>
<li>3、实现简单（可提取一个Query的层超类来完成自动填充）</li>
<li>4、便于IDE时的参数提示，同时可以提供默认值
<h1>[1.28]-COOKIE：对COOKIE原生态的支持及记忆加密升级版</h1>
<h4><a href="/wikis/%5B1.27%5D-%E7%B2%BE%E7%9B%8A%E5%BC%80%E5%8F%91%EF%BC%9A%E6%9B%B4%E5%AF%8C%E8%A1%A8%E7%8E%B0%E5%8A%9B%E7%9A%84Model%E5%B1%82%E5%92%8C%E9%87%8D%E9%87%8F%E7%BA%A7%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E7%9A%84%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.29%5D-%E5%BC%80%E6%94%BE%E4%B8%8E%E5%B0%81%E9%97%AD%EF%BC%9A%E5%A4%9A%E5%85%A5%E5%8F%A3%E5%92%8C%E7%BB%9F%E4%B8%80%E5%88%9D%E5%A7%8B%E5%8C%96.html">下一章</a></h4></li>
</ul>
<hr />
<p><em>只要你能把握自己，你就能把握任何东西。  -- 古谚语</em>  </p>
<h2>1.28.1 原生态的支持</h2>
<p>应开发同学的需求，这里补充对COOKIE的支持。  </p>
<p>因为，确实在项目的开发过程中，有时是需要对COOKIE进行处理的。所以，这里提供了简单的原生态支持。  </p>
<p>下面通过代码示例来简明如何使用。</p>
<h3>(1)注册COOKIE服务</h3>
<p>如同其他的服务一样，我们在使用前需要对COOKIE进行注册。不需要用到COOKIE的同学，可以忽略此服务的注册。  </p>
<p>在初始化文件init.php中添加如下注册代码：</p>
<pre><code class="language-javascript">//COOKIE
DI()-&gt;cookie = 'PhalApi_Cookie';</code></pre>
<h3>(2)使用</h3>
<p>查看PhalApi_Cookie提供的接口，可以得到针对COOKIE的三种操作：</p>
<pre><code class="language-javascript">//设置COOKIE服务
DI()-&gt;cookie-&gt;set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

//获取
echo DI()-&gt;cookie-&gt;get('name');  //输出 phalapi

//删除
DI()-&gt;cookie-&gt;delete('name');</code></pre>
<h2>1.28.2 记忆加密升级版</h2>
<p>实际情况，项目的使用的情况更为复杂。  </p>
<p>比如，需要对数据进行加解密，或者需要突破COOKIE的限制（设置后需要下一次请求才能获取，不过这也正是COOKIE的特点），想记住这一次设置的内容。所以，额外提供了这个升级版本。  </p>
<h3>(1)注册COOKIE服务</h3>
<p>和原生态的初始化，有些许不同，在于需要配置指定加解密的服务。如下：</p>
<pre><code class="language-javascript">$config = array('crypt' =&gt; $crypt, 'key' =&gt; 'a secrect');
DI()-&gt;cookie = new PhalApi_Cookie_Multi($config);</code></pre>
<p>上面中的crypt即为加密解服务，未指定时使用DI()-&gt;crypt，需要实现PhalApi_Crypt接口；key则为对应的密钥。</p>
<h3>(2)使用COOKIE服务</h3>
<p>使用同原生态一样，客户端感知不到区别，这里不再赘述。</p>
<h3>(3)一个简单的实现示例</h3>
<p>假设我们有这样一个简单的加解密类（真的很简单的）：</p>
<pre><code class="language-javascript">class Cookie_Crypt_Mock implements PhalApi_Crypt {

    public function encrypt($data, $key) {
        return base64_encode($data);
    }

    public function decrypt($data, $key) {
        return base64_decode($data);
    }
}</code></pre>
<p>随后，写一个简单的接口测试一下（因为涉及到COOKIE的操作，需要与浏览器可视化测试，所以没用单元测试）：</p>
<pre><code class="language-javascript">    public function cookieTest() {
        $rs = array();

        $config = array('crypt' =&gt; new Cookie_Crypt_Mock(), 'key' =&gt; 'a secrect');
        DI()-&gt;cookie = new PhalApi_Cookie_Multi($config);

        $rs['aEKey'] = DI()-&gt;cookie-&gt;get('name');
        DI()-&gt;cookie-&gt;set('name', 'phalapi', $_SERVER['REQUEST_TIME'] + 600);

        return $rs;

    }
</code></pre>
<p>运行效果如下：  </p>
<p><img src="http://webtools.qiniudn.com/20150411204629.jpg" alt="apic" />  </p>
<p>从上图中可以看出，COOKIE在浏览器已经被我们做了“加密”，哈哈~</p>
<h2>1.28.3 一如其他框架的建议</h2>
<ul>
<li>1、敏感数据不要存到COOKIE，以保证数据安全</li>
<li>2、大的数据不要存到COOKIE，以保证瘦的客户端</li>
</ul>
<h2>1.28.4 扩展你的COOKIE</h2>
<p>如需扩展，请重载PhalApi_Cookie基类。</p>
<hr />
<h4><a href="/wikis/%5B1.27%5D-%E7%B2%BE%E7%9B%8A%E5%BC%80%E5%8F%91%EF%BC%9A%E6%9B%B4%E5%AF%8C%E8%A1%A8%E7%8E%B0%E5%8A%9B%E7%9A%84Model%E5%B1%82%E5%92%8C%E9%87%8D%E9%87%8F%E7%BA%A7%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96%E7%9A%84%E5%BA%94%E5%AF%B9%E6%96%B9%E6%A1%88">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.29%5D-%E5%BC%80%E6%94%BE%E4%B8%8E%E5%B0%81%E9%97%AD%EF%BC%9A%E5%A4%9A%E5%85%A5%E5%8F%A3%E5%92%8C%E7%BB%9F%E4%B8%80%E5%88%9D%E5%A7%8B%E5%8C%96.html">下一章</a>#[1.29]-开放与封闭：多入口和统一初始化</h4>
<h4><a href="/wikis/%5B1.28%5D-COOKIE%EF%BC%9A%E5%AF%B9COOKIE%E5%8E%9F%E7%94%9F%E6%80%81%E7%9A%84%E6%94%AF%E6%8C%81%E5%8F%8A%E8%AE%B0%E5%BF%86%E5%8A%A0%E5%AF%86%E5%8D%87%E7%BA%A7%E7%89%88.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.30%5D-%E4%BF%9D%E6%8C%81%E7%9A%84%E5%8A%9B%E9%87%8F%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html">下一章</a></h4>
<hr />
<p><em>美国著名棒球运动员约格.贝拉说过：“通过仔细地观察，你可以了解很多事情。”</em>  </p>
<h2>1.29.1 绝对的开放，还是绝对的固化？</h2>
<p>框架应该都会考虑这样一个问题：到底是应该给应用项目提供统一固定的入口和初始化流程呢，还是应该给他们完全自由的空间？  </p>
<p>但我发现，很多PHP的框架都提供了一个绝对的固化流程。也就是你能作出改动的地方很少，虽然这样应用不需要过多地去考虑。  </p>
<p>然而我觉得，这样做，弊大于利。特别在现在项目需求背景各有各的不同时。所以，我为PhalApi框架选择了开放式的初始化做法。考虑到若开放的度太大，项目可能会迷茫，所以又结合了统一的初始化。  </p>
<p>下面分别说明这两点：开放式入口和封闭式的初始化。  </p>
<h2>1.29.2 开放式的入口</h2>
<p>作为一个接口系统，是需要为不同的终端、不同的开放人群，甚至不同的版本提供不同的服务的。  </p>
<p>如：  </p>
<ul>
<li>按不同的终端：iOS设备、Android设备、PC版、网页版</li>
<li>按不同的开放人群：手机客户端、内部管理后台、内部其他系统、公共开放平台</li>
<li>按不同的版本：版本从V1、V2到VN  </li>
<li>按不同的项目：同一个应用下可能会存在多个项目，当然更推荐每个项目单独一个系统</li>
</ul>
<p>基于此，为不同的维度提供不同的入口就很有现实实用场景了。  </p>
<p>更为重要的是，这些不同的入口都应该尽可能简单，并能统一共享公共的组件资源、数据库、日记系统这些。一个可能的入口示例如下：  </p>
<pre><code class="language-javascript">&lt;?php
/**
 * Demo 统一入口
 */

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()-&gt;loader-&gt;addDirs('Demo');

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api-&gt;response();
$rs-&gt;output();</code></pre>
<p>简单解读一下上面的代码，首先要加载统一初始化文件，其实装载挂靠的接口项目（对应你的项目目录的名称），最后创建一个PhalApi接口实例进行响应、输出结果。  </p>
<p>下面来看下统一初始化文件需要做的事情。</p>
<h2>1.29.3 封闭式的初始化</h2>
<p>不言而喻，尽管我们有按不同维度划分入口的需要，但统一初始化的过程更是必不可少的。  </p>
<p>不同的入口提供了各维度定制的机会，统一的初始化则为应用提供了统一定制的机会。  </p>
<h3>(1)常规入口代码</h3>
<p>常规的入口，需要以下初始化操作：</p>
<pre><code class="language-javascript">&lt;?php
/**
 * 统一初始化
 */

/** ---------------- 根目录定义，自动加载 ---------------- **/

date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');

require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);

/** ---------------- 注册&amp;初始化服务组件 ---------------- **/

//自动加载
DI()-&gt;loader = $loader;

//配置
DI()-&gt;config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', 
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()-&gt;notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};

//调试模式，$_GET['__debug__']可自行改名
DI()-&gt;debug = !empty($_GET['__debug__']) ? true : DI()-&gt;config-&gt;get('sys.debug');

//翻译语言包设定
SL('zh_cn');</code></pre>
<h3>(2)常规代码解读</h3>
<p>上面是框架执行所需的基础服务注册和配置，一般直接可用，但也可以根据需要作些细微的调整。如日记的级别设定、调试的参数修改（改成一个只有自己知道的参数名字，别让外界知道！）等。 </p>
<p>出于让大家对初始化过程有一个更理性的认识，这里补充一下各代码的作用。  </p>
<p>最开始是利用了PHP原生态的时区设置和宏定义：  </p>
<pre><code class="language-javascript">date_default_timezone_set('Asia/Shanghai');

defined('API_ROOT') || define('API_ROOT', dirname(__FILE__) . '/..');</code></pre>
<p>接着，便开始引入PhalApi框架的类自动加载器：</p>
<pre><code class="language-javascript">require_once API_ROOT . '/PhalApi/PhalApi.php';
$loader = new PhalApi_Loader(API_ROOT);</code></pre>
<p>这样，我们就可以从原生态的PHP开发，切入到了PhalApi接口开发模式。但在捲起袖口准备大干一场前，我们还需要注册一些必备的服务：</p>
<pre><code class="language-javascript">//自动加载
DI()-&gt;loader = $loader;

//配置
DI()-&gt;config = new PhalApi_Config_File(API_ROOT . '/Config');

//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', 
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);

//数据操作 - 基于NotORM，$_GET['__sql__']可自行改名
DI()-&gt;notorm = function() {
    $debug = !empty($_GET['__sql__']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};</code></pre>
<p>上面的自动加载、配置、日记和数据库操作通常而言，对于一个项目都是必须的。<br />
但配置文件的路径可自行指定，日记的存储类型也可以自由组合（多个类型采用或运算），还可以选择你合适的数据库配置。
注意到，PhalApi_DB_NotORM初始化时，除了配置文件外，还有一个debug参数，此参数的作用是用于控制是否打印显示执行的SQL语句及对应消耗的时间。  </p>
<p>至此，我们已经为项目完成了绝大部分的基础服务注册，且上面的初始化顺序建议保留不变。因为，前后有依赖关系。  </p>
<p>但为了让我们的项目更有活力、更具生气、更国际化，我们还可以多加这么两行代码：</p>
<pre><code class="language-javascript">//调试模式，$_GET['__debug__']可自行改名
DI()-&gt;debug = !empty($_GET['__debug__']) ? true : DI()-&gt;config-&gt;get('sys.debug');

//翻译语言包设定
SL('zh_cn');</code></pre>
<p>这里，也有一个debug参数，之所以和数据库的分开，是因为如果混在一起会导致返回结果解析失败（如不再是JSON格式）。<br />
此debug的来源，默认来自环境的系统配置文件（如区分生产环境和测试环境）；也可以来自某个请求的手动设置，这样，开发同学便可以快速进行在线调试了。
而这个参数，则是框架代码、项目代码以及扩展类库所共用的调试开关，至于各个场景使用的效果，视各环节而定。  </p>
<p>SL()是一个快速函数，作用是设定翻译语言包。如果觉得中文下的UTF-8查看不直观，可以自行加个参数修改，如：</p>
<pre><code class="language-javascript">SL(isset($_GET['__lan__']) ? $_GET['__lan__'] : 'zh_cn');</code></pre>
<h3>(3)更多定制注册代码</h3>
<p>上面的操作，涵盖了大部分项目的需要。除此之外，还有一些额外的服务，可根据自身的情况，定制处理：</p>
<pre><code class="language-javascript">/** ---------------- 以下服务组件就根据需要定制注册 ---------------- **/

//缓存 - MC
/**
DI()-&gt;cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()-&gt;config-&gt;get('sys.mc'));
    return $mc;
};
 */

//签名验证服务
//DI()-&gt;filter = 'Common_SignFilter';

//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()-&gt;response = new PhalApi_Response_JsonP($_GET['callback']);
}</code></pre>
<h3>(4)更多定制解读</h3>
<p>此部分的注册，非项目必须部分。可根据需要，自行定制。  </p>
<p>如上面出现的第一个，即缓存服务，使用的是Memcached：</p>
<pre><code class="language-javascript">DI()-&gt;cache = function() {
    $mc = new PhalApi_Cache_Memcached(DI()-&gt;config-&gt;get('sys.mc'));
    return $mc;
};</code></pre>
<p>接下来的，是重要的接口签名验证服务。之所以没有提供这个服务的实现，是出于更高安全性考虑而建议项目各自制定签名规则并实现。然后这样简单注册即可被框架自动调用：</p>
<pre><code class="language-javascript">//签名验证服务
//DI()-&gt;filter = 'Common_SignFilter';</code></pre>
<p>在有些需要支持JsonP返回的场景，可以使用PhalApi_Response_JsonP返回格式开放callback操作：</p>
<pre><code class="language-javascript">//支持JsonP的返回
if (!empty($_GET['callback'])) {
    DI()-&gt;response = new PhalApi_Response_JsonP($_GET['callback']);
}</code></pre>
<h2>1.29.4 用一张图来表示</h2>
<p><img src="http://webtools.qiniudn.com/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98%E9%9D%99%E6%80%81%E7%BB%93%E6%9E%84%E5%9B%BE-%E5%A4%9A%E5%85%A5%E5%8F%A3.jpg" alt="多级缓存静态结构图-多入口" /></p>
<hr />
<h4><a href="/wikis/%5B1.28%5D-COOKIE%EF%BC%9A%E5%AF%B9COOKIE%E5%8E%9F%E7%94%9F%E6%80%81%E7%9A%84%E6%94%AF%E6%8C%81%E5%8F%8A%E8%AE%B0%E5%BF%86%E5%8A%A0%E5%AF%86%E5%8D%87%E7%BA%A7%E7%89%88.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.30%5D-%E4%BF%9D%E6%8C%81%E7%9A%84%E5%8A%9B%E9%87%8F%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html">下一章</a>#[1.30]-保持的力量：接口开发最佳实践</h4>
<h4><a href="/wikis/%5B1.29%5D-%E5%BC%80%E6%94%BE%E4%B8%8E%E5%B0%81%E9%97%AD%EF%BC%9A%E5%A4%9A%E5%85%A5%E5%8F%A3%E5%92%8C%E7%BB%9F%E4%B8%80%E5%88%9D%E5%A7%8B%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">下一章</a></h4>
<hr />
<p><em>神啊，求你赐给我平静的心，去接受我无法改变的事；赐给我勇气，去做我能改变的事；赐给我智慧，去分辨两者的不同。 --平静之祷</em>  </p>
<h2>1.30.1 论保持的力量</h2>
<p>追到一个心仪的女生不难，难于如何保持和培养一份真挚的感情；获得一时的财富也不难，难于如何长久保持收益；创业的公司很容易博得一时媒体的关注以及某次天使的投资，但难于如何排除各种障碍、充分利用各方资源发展成中企业及至上市公司。  </p>
<p>同样，提供一时的接口很容易，但当我们需要不断为接口提供升级，以及当我们维护提供一整套接口时，面临的困难和问题会越来越大。<br />
所以，这是一场持久的战役。需要我们用稳重的心态、专业的能力在背后持久支撑、推动。  </p>
<p>值得庆幸的是，这些都是问题而不是限制，都是可以被解决的。<br />
以下是结合 @郭了个浩浩 同学提供的apigee.web_api.pdf文档，以及我们多年来的项目实际开发经验为新手提供的一些建议，对老同学相信也会有所帮助。<br />
每个建议通常会包括三部分： <strong>现在主流的做法、PhalApi的做法以及项目的选取。 </strong></p>
<h2>1.30.2 最佳实践建议</h2>
<p>为了大家查阅和翻看，这里先罗列本章的全部建议：</p>
<ul>
<li>(1)接口风格和协议的选择 - HTTP</li>
<li>(2)接口域名 - 使用api单独域名</li>
<li>(3)异常处理 - 200/400/500三大接口结果状态码</li>
<li>(4)对外的命名规则 - 使用小写加下划线</li>
<li>(5)对内的命名规则 - 使用驼峰法和遵循PEAR命名</li>
<li>(6)安全与验证 - 使用接口签名和token登录态双重机制</li>
<li>(7)返回结果格式 - JSON</li>
<li>(8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口</li>
<li>(9)SDK包 - 给客户端自由的调用空间和自由</li>
<li>(10)接口文档 - 使用markdown快速编写</li>
<li>(11)测试驱动开发 - 坚持单元测试</li>
</ul>
<h2>1.30.3 建议细说</h2>
<h3>(1)接口风格和协议的选择 - HTTP</h3>
<p>目前，后台接口开发可以用RESTFull风格，也可以用Web Service；可以用SOAP协议、RPC协议，也可以用HTTP协议；可以用短链接，也可以使用长链接。如果我们希望继续进行划分，还可以分为同步或异步、单个或批量、是否有SDK包、内部接口还是开放接口平台等。  </p>
<h4>主流的做法</h4>
<p>现在看来，大部分大型的企业以及大多数的小公司使用的都是HTTP协议下的接口开发，部分使用RESTFull，但Web Service较少。如：  </p>
<ul>
<li><a href="http://open.youku.com/docs/docs?id=44">优酷开放平台</a>，<a href="https://openapi.youku.com/v2/videos/show_basic.json">示例请点击</a></li>
<li><a href="http://iot.weixin.qq.com/index.html">微信公众号-服务器接口协议</a>，<a href="https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&amp;appid=">示例请点击</a></li>
<li><a href="open.weibo.com">新浪微博开放平台</a>，使用REST风格，<a href="https://api.weibo.com/2/statuses/mentions/ids.json">示例请点击</a></li>
<li><a href="http://aws.amazon.com/cn/documentation/">Amazon</a> ，采用Web Service、SOAP、REST等多种风格和协议提供服务，<a href="http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGETtagging.html">示例请点击</a></li>
</ul>
<h4>PhalApi的做法</h4>
<p>我们选取了HTTP的协议，在于其无论是客户端接入、开发调试，还是部署构建上都很容易实现，而且也符合主流，因为大家都比较熟悉。<br />
这一点是非常重要的：因为简单，后台接口开发的同学才会更容易上手；因为容易，客户端接入才会更加无压力而不用担心处处受挫。  </p>
<h4>项目的选取</h4>
<p>根据项目不同的项目背景和需求，可以选择你合适的风格或者协议。但是即使出于安全、性能或者其他技术或非技术的原因而不采用HTTP协议的情况下，你也可以在PhalApi原有的接口开发实现时，轻松扩展你需要的协议。如使用SOAP，PHPRpc或者swoole下的TCP协议。其中，部分协议已有扩展类库提供支持。</p>
<h3>(2)接口域名 - 使用api单独域名</h3>
<p>首先，有一点是可以肯定的。<br />
接口系统应该有自己单独的域名，而不应该附属于网站或者管理后台。  </p>
<h4>主流的做法</h4>
<p>显然，主流做法也是这样做的。如：</p>
<ul>
<li>优酷开放平台：<a href="https://openapi.youku.com">https://openapi.youku.com</a></li>
<li>微信公众号： <a href="https://api.weixin.qq.com">https://api.weixin.qq.com</a></li>
<li>新浪微博： <a href="https://api.weibo.com">https://api.weibo.com</a></li>
</ul>
<h4>项目的选取</h4>
<p>如果可以，尽量让接口系统使用独立的域名，并且使用api作为一级域名。如：</p>
<pre><code>//你的网站为：
http://www.demo.com

//则对应的接口为：
http://api.demo.com</code></pre>
<h3>(3)异常处理 - 200/400/500三大接口结果状态码</h3>
<p>对于接口的异常处理，在使用HTTP协议下，可以通过HTTP本身的响应状态码来进行区分。
在非HTTP协议并有SDK包的情况下，异常的处理手段则会更为多样。  </p>
<h4>主流的做法</h4>
<p>优酷接口采用了HTTP响应状态码加结果返回的形式，如：</p>
<pre><code class="language-javascript">Request URL:https://openapi.youku.com/v2/videos/show_basic.json
Request Method:GET
Status Code:400 Bad Request

{"error":{"code":1004,"type":"SystemException","description":"Client id null"}}</code></pre>
<p>新浪微博也一样：</p>
<pre><code class="language-javascript">Request URL:https://api.weibo.com/2/statuses/mentions/ids.json
Request Method:GET
Status Code:403 Forbidden

{"error":"auth by Null spi!","error_code":21301,"request":"/2/statuses/mentions/ids.json"}</code></pre>
<p>微信接口则采用了统一200的形式，如：</p>
<pre><code class="language-javascript">Request URL:https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&amp;appid=
Request Method:GET
Status Code:200 OK

{"errcode":41002,"errmsg":"appid missing"}</code></pre>
<h3>PhalApi的做法</h3>
<p>为了与HTTP保持一致性，同时降低不必要的复杂性，我们采用了200/400/500三大接口结果状态码。  </p>
<p>注意，这里所说的三大状态码，是指接口返回结果中的状态码，而不是HTTP的响应状态。<br />
也就是说接口全部的结果返回都应该是200，除非接口服务有内部未捕获的异常，即：</p>
<pre><code class="language-javascript">Status Code:200 OK</code></pre>
<p>返回结果状态码刚是以下几种：</p>
<pre><code class="language-javascript">//正常返回
{
    "ret": 200,
    "data": {
        //...
    },
    "msg": ""
}

//客户端非法请求
{
    "ret": 400,
    "data": [],
    "msg": "非法请求：接口服务Default.Test不存在"
}

//服务端内部错误
{
    "ret": 500,
    "data": [],
    "msg": "服务器运行错误: can not connect to database db_demo"
}

####项目的选取
你可以根据你的需要，扩展400和500这两系列的错误，如401表示登录失败等。  
此外，在data里面，你也可以添加一个code来表示业务级的操作码，以及客户端根据不同的业务场景做出不同和反应、交互或引导提示。
</code></pre>
<h3>(4)对外的命名规则 - 使用小写加下划线</h3>
<p>对外的命名，是指外部看得到的命名，如接口参数的名字，接口返回的结果节点名字，以及数据库的表名、字段名。  </p>
<h4>主流的做法</h4>
<p>新浪微博采用了小写加下划线的做法，如：</p>
<pre><code class="language-javascript">//URL
https://c.api.weibo.com/2/friendships/followers/trend_count.json

//请求参数
source
access_token

//返回结果
{
    "uid": 10438,
    "result": [            
        {
            "days": "2012-04-04",
            "follower_count_online":"15",  //粉丝数 
            "active_follower":"14", //活跃粉丝数
            "loyal_follower":"0"   //互动粉丝数            
        },
        ....
    ]
}
</code></pre>
<p>Amazon采用了首字母大写且无下划线的做法，如：</p>
<pre><code class="language-javascript">//Responses
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2009 12:00:00 GMT
Connection: close
Server: AmazonS3

&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;BucketLoggingStatus xmlns="http://doc.s3.amazonaws.com/2006-03-01"&gt;
  &lt;LoggingEnabled&gt;
    &lt;TargetBucket&gt;mybucketlogs&lt;/TargetBucket&gt;
    &lt;TargetPrefix&gt;mybucket-access_log-/&lt;/TargetPrefix&gt;
    &lt;TargetGrants&gt;
      &lt;Grant&gt;
        &lt;Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:type="AmazonCustomerByEmail"&gt;
          &lt;EmailAddress&gt;user@company.com&lt;/EmailAddress&gt;
        &lt;/Grantee&gt;
        &lt;Permission&gt;READ&lt;/Permission&gt;
      &lt;/Grant&gt;
    &lt;/TargetGrants&gt;
  &lt;/LoggingEnabled&gt;
&lt;/BucketLoggingStatus&gt;</code></pre>
<h3>PhalApi的做法</h3>
<p>我们提倡使用全部小写加下划线的命名，因为这样更符合客户端的使用，如：
接口参数：</p>
<pre><code class="language-javascript">//正确的
&amp;user_id=888

//错误的
&amp;userId=888</code></pre>
<p>返回字段：</p>
<pre><code class="language-javascript">//正确的
"device_type": "cube",

//错误的
"deviceType": "cube",</code></pre>
<p>数据库字段：</p>
<pre><code class="language-javascript">//正确的
`user_id` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',

//错误的
`userId` bigint(20) DEFAULT '0' COMMENT '创建者的用户ID',</code></pre>
<h3>项目的选取</h3>
<p>不管是使用全部小写，还是全部大写，项目都应该保持一致的命名风格，而不是混合凌乱的风格。</p>
<h3>(5)对内的命名规则 - 使用驼峰法和遵循PEAR命名</h3>
<p>与对外命名对应的则是对内的命名规则，这里又回归到了老生常谈的PHP代码风格。<br />
这里不作过多的说明，只是稍作提及。  </p>
<h3>PhalApi的做法</h3>
<p>我们建议使用PEAR包的命名风格，和驼峰法，如下为一个接口示例：</p>
<pre><code class="language-javascript">$ vim ./Api/Default.php 
&lt;?php
/**
 * 默认接口服务类
 *
 * @author: dogstar &lt;chanzonghuang@gmail.com&gt; 2014-10-04
 */

class Api_Default extends PhalApi_Api {

    public function getRules() {
        return array(
            'index' =&gt; array(
                'username'  =&gt; array('name' =&gt; 'username', 'default' =&gt; 'PHPer', ),
            ),
        );
    }

    public function index() {
        return array(
            'title' =&gt; 'Default Api',
            'content' =&gt; T('Hello {name}, Welcome to use PhalApi!', array('name' =&gt; $this-&gt;username)),
            'version' =&gt; PHALAPI_VERSION,
            'time' =&gt; $_SERVER['REQUEST_TIME'],
        );
    }
}</code></pre>
<h3>项目的选取</h3>
<p>你可以选择你喜欢的风格，但团队应该保持一致。  </p>
<p>即便你不喜欢PhalApi约定的PEAR命名，你也可以自行实现内部的类加载机制。<br />
当有多个项目或者多个模块并存时，可以添加模块名前缀来作区分，如：</p>
<pre><code class="language-javascript">$ tree
.
├── Demo
│   └── Api
│       └── DUser.php
├── MyApp
│   └── Api
│       └── MUser.php
└── Task
    └── Api
        └── TUser.php

$ head */*/*
==&gt; Demo/Api/DUser.php &lt;==
&lt;?php
class Api_DUser extends PhalApi_Api {
}

==&gt; MyApp/Api/MUser.php &lt;==
&lt;?php
class Api_MUser extends PhalApi_Api {
}

==&gt; Task/Api/TUser.php &lt;==
&lt;?php
class Api_TUser extends PhalApi_Api {
}</code></pre>
<p>其它的Domain层和Model层等也类似，这样可以避免类名冲突，或者IDE开发环境下的混淆。</p>
<h3>(6)安全与验证 - 使用接口签名和token登录态双重机制</h3>
<p>既然采用HTTP协议，那么安全方面就需要接口自身进行保证。  </p>
<p>所幸，现在可用的加密手段有多种选择。<br />
对于接口签名，我们可以使用非对称的验签方式，如md5；也可以用对称的方式，如RSA。<br />
最后，为每一个接入的客户端分配app_key和app_secrect即可。  </p>
<p>当然，更好的安全是接口系统再提供登录态的验证，即通常所说的token。
这两者的相合，会为接口增加更好的安全保障。</p>
<h4>主流的做法</h4>
<ul>
<li>七牛云存储，采用Access Key/Secret Key，并且在需要时添加相应的凭证</li>
<li>微信公众号，采用由AppID(应用ID)和AppSecret(应用密钥)生成的ACCESS_TOKEN</li>
<li>优酷开放平台，采用应用Key client_id</li>
</ul>
<h3>PhalApi的做法</h3>
<p>我们不提供具体的接口签名方案，是因为把这种决策移交给项目应用本身进行定制。<br />
而定制也是非常简单的，只需要简单的两步即可：  </p>
<ul>
<li>1、实现过滤器接口 <strong>PhalApi_Filter::check()</strong>；</li>
<li>2、注册过滤器服务 <strong>DI()-&gt;filter</strong>；</li>
</ul>
<p>对于token，虽然框架没有提供内置的实现，但可以从PhalApi的扩展类库寻找这种支持，这一点已经User扩展类库支持。  </p>
<h3>项目的选取</h3>
<p>正如PhalApi提供的自由空间，项目可以自行实现接口签名，和根据需要是否采用User扩展类库，或者自行实现token的处理。</p>
<h3>(7)返回结果格式 - JSON</h3>
<p>在 <a href="/wikis/%5B1.14%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E8%BF%94%E5%9B%9E%E6%A0%BC%E5%BC%8F%E5%92%8C%E7%BB%93%E6%9E%84%EF%BC%9Aret-data-msg.html">[1.14.1 统一返回的格式]</a>一节中，已经对JSON的返回格式作了说明，这里不再赘述，也只是稍作提及。  </p>
<h4>主流的做法</h4>
<p>目前采用了JSON的格式返回的有：</p>
<ul>
<li>新浪微博</li>
<li>优酷开放平台</li>
<li>腾讯开放平台</li>
<li>微信接口</li>
</ul>
<p>采用了XML格式返回的有：</p>
<ul>
<li>Amazon</li>
</ul>
<h3>PhalApi的做法</h3>
<p>我们默认采了JSON的格式返回。</p>
<h3>项目的选取</h3>
<p>项目可以轻松扩展成其他格式的返回。  </p>
<h3>(8)URL规则与路由映射 - 统一service接口服务，可一个文件一个接口</h3>
<p>先从项目内部的文件划分说起，通常最为常见的情况是，很多开发人员都喜欢把很多很多很多接口都塞到一个接口文件里面。  </p>
<p>这样的文件，通常会有2K到3K左右。<br />
我觉得这是一种极端，而且是一种不好的极端。因为文件过大的话，会带来很多问题。  </p>
<p>但与之对立的有另一种做法，即一个文件，一个接口。<br />
这一点，在我之前就职的一家出名的游戏公司中得到了广泛的认可和遵循。如：  </p>
<pre><code class="language-javascript">//?service=UserInfo.Go
&lt;?php
class Api_UserInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}

//?service=GroupInfo.Go
&lt;?php
class Api_GroupInfo extends PhalApi_Api {

     public function go() {
           //TODO
     }
}</code></pre>
<p>虽然也是一种极端，但却很好地做到了接口隔离，即不用担心修改此接口的实现而影响到其他接口服务。  </p>
<p>最后，我们再来聊URL规则，就更顺畅了。如果我们采用一个文件对应一个接口，则我们可以省略Action（全部都为go()方法），简写成：?service=XXX。<br />
再进一步，我们可以利用接口服务器（如Nginx）的规则Rewrite来提供更好的URL规则，同时尽量隐藏我们的接口内部实现细节，如：</p>
<pre><code class="language-javascript">//原始地
http://api.demo.com/?service=UserInfo.Go

//简化地
http://api.demo.com/?service=UserInfo

//再进一步
http://api.demo.com/UserInfo

//或者
http://api.demo.com/UserInfo.json</code></pre>
<p>还有一点需要关注的就是接口的版本，当有v1，v2，v3等不同的版本时，我们也需要在接口URL中体现这些版本的不同。  </p>
<h4>主流的做法</h4>
<ul>
<li>新浪微博的一个接口URL：<a href="https://c.api.weibo.com/2/friendships/followers/trend_count.json">https://c.api.weibo.com/2/friendships/followers/trend_count.json</a></li>
<li>优酷平台的一个接口URL：<a href="https://openapi.youku.com/v2/videos/show_basic.json">https://openapi.youku.com/v2/videos/show_basic.json</a></li>
<li>微信的一个接口URL：<a href="https://api.weixin.qq.com/device/get_stat">https://api.weixin.qq.com/device/get_stat</a></li>
</ul>
<h3>PhalApi的做法</h3>
<p>目前而言，PhalApi在URL规则和路由这块还比较欠缺，没有像其他网站一样提供强大的路由支持。<br />
但我们在代码实现的层面，可以提供不同的入口，以开放给不同的终端（内部的或者外部的）， 以及不同的版本支持。如：</p>
<pre><code class="language-javascript">$ tree Public/
Public/
├── v1
│   └── index.php
├── v2
│   └── index.php
└── v3
    └── index.php

3 directories, 3 files</code></pre>
<p>则对应的版本URL则可以为：</p>
<pre><code class="language-javascript">//v1版本
http://api.demo.com/v1/?service=Default.Index

//v2版本
http://api.demo.com/v2/?service=Default.Index

//v3版本
http://api.demo.com/v3/?service=Default.Index
</code></pre>
<h3>项目的选取</h3>
<p>项目可以结合不同的入口，以及接口服务器的URL规则Rewrite作一些自定的URL路由。</p>
<h3>(9)SDK包 - 给客户端自由的调用空间和自由</h3>
<p>目前移动开发主要有iOS、Android、Windowns Phone、网站等不同的终端，各种终端又有不同的语言，如果我们需要提供SDK包，不仅仅需要考虑到纵向的版本升级，还需要维护横向的多样性。<br />
而且，如果我们使用的是HTTP协议，则不必要担心这些维护的成本，同时给客户端提供一个自由的空间进行调用 -- 即客户端可以自己编写本身的接口客户端。  </p>
<h4>主流的做法</h4>
<p>很多国内的开放平台接口都是不提供SDK包的，但有些安全度高的则会，如支持宝。  </p>
<p>以下是一些提供了SDK的平台 ：</p>
<ul>
<li><a href="https://openhome.alipay.com/doc/docIndex.htm?url=https://openhome.alipay.com/doc/viewKbDoc.htm?key=236698_261849&amp;type=info">支付宝SDK</a></li>
<li><a href="https://developer.yahoo.com/social/sdk/">Yahoo! SDKs</a></li>
<li><a href="https://aws.amazon.com/cn/tools/">Amazon丰富的开发工具包</a></li>
</ul>
<h3>PhalApi的做法</h3>
<p>我们暂时没有提供SDK包，但对于PHP，有一个简单的客户端类，可见： <a href="/wikis/%5B1.13%5D-%E7%BB%9F%E4%B8%80%E7%9A%84%E6%8E%A5%E5%8F%A3%E8%AF%B7%E6%B1%82%E6%96%B9%E5%BC%8F%EF%BC%9A_sevice=XXX.XXX.html">[1.13]-统一的接口请求方式</a></p>
<h3>项目的选取</h3>
<p>出于公司产品簇的项目考虑，项目可以内部提供SDK给同类的客户端使用，如分为iOS版的客户端SDK，以及Android版的客户端SDK。</p>
<h4>小故事：与SDK包的一个真实的痛苦经历</h4>
<p>有一点是非常重要的，千万不要让不懂PHP语言的人去开发提供PHP的SDK包，更不要使用所谓的工具自动转换生成SDK包代码。<br />
在我曾经做过的一个项目中，因为需要接入一个接口系统，而这个接口是由专业的JAVA团队维护的，但他们对PHP语言则是非常薄弱，以致他们使用了工具来生成PHP语言的SDK包。   </p>
<p>这就导致了我在接入一个简单的接口时，却开发联调耗费了两天、测试联调时耗费了在接口调用超时问题排查上。<br />
而最后找到的原因却是因为app_key不对而导致服务端异常，而在SDK包却隐藏了这一异常错误信息，反而给出了time out超时的提示，严重误导了排查的方向！<br />
而当我尝试深入去调试SDK时，得到却又是既没有code又没有message的异常！最让人难以忍受的是，他们提供的SDK包竟然和JAVA的企业系统一样复杂的结构（正如他们是使用工具来生成转换的）！  </p>
<p>想象一下，PHP代码下有\com\sina\webo\sdk\Constants.php这样类似JAVA的文件结构，PHP的同学会作何感想？用JAVA的世界的方式来开发PHP，显然是走不通的啊！<br />
而执意要走的话，到最后就是各种接入的痛苦，稍微按奈不住的同学难免就会因为情绪问题而大开争论了。而这一切，只是因为非PHP人员使用了自动生成工具。<br />
我觉得，这是一种不负责任的做法，希望大家不要效仿。</p>
<h3>(10)接口文档 - 使用markdown快速编写</h3>
<p>（场外音：通过沐浴法理清了头绪，继续回来执笔编写）。<br />
就我个人经历而言，markdown就是一个开始你会拒绝，接着你会越来越喜欢，到最后会爱不释手的一个工具。  </p>
<p>如果你或者你的团队还在使用邮件或者work文档来传递共享接口文档，那就太不应该了；如果你正在使用某个WIKI系统进行文档的维护但却不喜欢它的编辑或者展示方式时，你可以尝试使用一下markdown。<br />
正如你现在正在查看的文档也是通过markdown编写的。  </p>
<h4>主流的做法</h4>
<p>作为开放接口平台，文档肯定是以网站的形式提供。但很多时候，对于我们内部的接口或者小项目来说，显然这样的成本太大了。<br />
接口，从简单开始。<br />
我们理应一直坚持这一点，所以文档也是一样，我们应该寻求一种在内部快速共享最新接口文档的途径。如：</p>
<ul>
<li>1、使用内部WIKI</li>
<li>2、使用开源中国或者其他站点的WIKI（这时可以通过在线编辑或者GIT更新）</li>
</ul>
<h3>项目的选取</h3>
<p>你可以根据项目的需要，或者公司以往的做法，但至少不要再使用邮件或者word文档。  </p>
<h3>(11)测试驱动开发 - 坚持单元测试</h3>
<p>单元测试，在PhalApi里面不只一次提到了，这里再次进行说明，是希望能引起大家的关注，去尝试体验一下。  </p>
<p>我们都知道，在开发一个新功能时、新接口时，修复一个BUG或者作一些大的调整或者重构工作，我们是毫无压力的，而且这时的成本很低，仅在于开发人员本身的时间和精力的消耗。<br />
当提测后进入测试阶段，测试人员发现一个BUG后，有些团队会以禅道或者Bugzilla或其他方式来纪录和追踪BUG。这时我们开发会觉得一个这么小的问题还需要去纪录、去登记很不值得。然后，我们应当注意到这时修复一个BUG会涉及到测试人员资源的开销。<br />
当进入了回归测试阶段，特别是多系统交互、跨团队合作时，一个BUG就会从一个人传到另一个人，从这个团队流到那个团队，这时成本就会逐渐增大。  </p>
<p>最后，上线后，当一个奇怪的问题出现后，我们需要定位原因就更加困难重重了。<br />
我曾经就经历这样一番：有用户发现游戏的道具减少了。我们一开始以为是某些运营配置、或者数据以及用户的等级限制所引发的，但在排除了各种业务的问题后，到最后却发现是PHP中array使用“+”运算而引发的血案！<br />
在正常情况下，我们都知道array_merge()函数对于数值的下标则会追加并重新生成下标序列，即会合并；而数组+则会去掉相同下标的元素。  </p>
<p>但实际情况下，线上BUG所产生的影响不在于排查和修复的时间成本，而在于在这段时间内所损失的金额、数据等成本。  </p>
<p>当然，从测试的角度上看，测试并不能保证我们的系统没有BUG，只能说暂时未发现BUG。<br />
单元测试也一样，作为开发人员，我们应当在最低成本的时期就及时发现我们直觉觉得可能会出现的问题并进行修复。<br />
对我们亲手所编写的代码负责，并且用客观的方式来证明我们的代码目前未发现问题，而不是主观认为“我写的代码没有问题”。更不应该一次又一次地犯下各种低级或者重复的错误，而让团队其他成员对我们丧失信任。  </p>
<p>PhalApi一直很注重单元测试，也很注重自动化，为了减轻大家重复编写单元测试骨架代码的痛苦，我们提供了一个可以生成单元测试代码的脚本。<br />
假设我们有这么一个类：</p>
<pre><code class="language-javascript">&lt;?php

class Api_Default extends PhalApi_Api {

        public function index() {
             //TODO
        }
}</code></pre>
<p>那么，我们可以这样生成测试代码：</p>
<pre><code class="language-javascript">$ cd .//Demo/Tests
$ phalapi-buildtest ../Api/Default.php Api_Default ./test_env.php 
&lt;?php
/**
 * PhpUnderControl_ApiDefault_Test
 *
 * 针对 ../Api/Default.php Api_Default 类的PHPUnit单元测试
 *
 * @author: dogstar 20150514
 */

require_once dirname(__FILE__) . '/test_env.php';

if (!class_exists('Api_Default')) {
    require dirname(__FILE__) . '/../Api/Default.php';
}

class PhpUnderControl_ApiDefault_Test extends PHPUnit_Framework_TestCase
{
    public $apiDefault;

    protected function setUp()
    {
        parent::setUp();

        $this-&gt;apiDefault = new Api_Default();
    }

    protected function tearDown()
    {
    }

    /**
     * @group testGetRules
     */ 
    public function testGetRules()
    {
        $rs = $this-&gt;apiDefault-&gt;getRules();
    }

    /**
     * @group testIndex
     */ 
    public function testIndex()
    {
        $rs = $this-&gt;apiDefault-&gt;index();
    }

}</code></pre>
<blockquote>
<p>温馨提示：  </p>
<ol>
<li>可以先执行：ln -s /path/to/PhalApi/phalapi-buildtest /usr/bin/phalapi-buildtest  </li>
<li>test_env.php为测试环境初始化文件，可以在里面引用init.php文件，并作一些调整  </li>
<li>输出的测试代码可以重定向到./Demo/Tests/Api/Api_Default_Test.php，让测试代码与产品代码对齐  </li>
</ol>
</blockquote>
<p>最后，我们就可以这样执行单元测试了：</p>
<pre><code class="language-javascript">$ phpunit ./Api_Default_Test.php </code></pre>
<hr />
<h4><a href="/wikis/%5B1.29%5D-%E5%BC%80%E6%94%BE%E4%B8%8E%E5%B0%81%E9%97%AD%EF%BC%9A%E5%A4%9A%E5%85%A5%E5%8F%A3%E5%92%8C%E7%BB%9F%E4%B8%80%E5%88%9D%E5%A7%8B%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">下一章</a>#[1.31]-新型计划任务：以接口形式实现的计划任务</h4>
<h4><a href="/wikis/%5B1.30%5D-%E4%BF%9D%E6%8C%81%E7%9A%84%E5%8A%9B%E9%87%8F%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.11%5D-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%EF%BC%9ADI%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5-%E8%AE%A9%E8%B5%84%E6%BA%90%E6%9B%B4%E5%8F%AF%E6%8E%A7.html">下一章</a></h4>
<hr />
<h2>1.31.1 这里所说的计划任务</h2>
<p>计划任务主要负责处理一些耗时的操作，或者非用户触发的作业。  </p>
<p>有些人会称它为后台任务，或者推送作业，又或者定时任务。这时则统称为：计划任务。  </p>
<p>例如，当你发布一条微信朋友圈后需要通知上百个好友时；当一条后台的推荐资讯需要推送到每个用户的客户端时；当需要将本地的静态资源如图片同步到CDN时。<br />
显然这些动则需要分钟级别的操作，不应该在客户端调用接口时同步处理（但让我惊讶的是现实真的有人会这么做！），又或者非用户触发而需要后台处理（但更让我惊讶的是竟然也有系统是在用户请求时附带进行处理，而且还是国内某个知名的会员中心！）。  </p>
<p>这里不仅仅是提供实现计划任务的约束和机制，更多的是引导大家更好地应对此类问题。</p>
<h2>1.31.2 计划任务的关键环节</h2>
<h3>(1)触发</h3>
<p>首先，是何时何地由何用户产生一条待执行的计划任务，我们可以把这个场景点称为一个触发点。<br />
通常的做法，我们会先纪录下此触发点的场景信息，并放入到一个队列里面，以便等待计划任务消费。  </p>
<h3>(2)调度</h3>
<p>其次，是通过何种机制进行计划任务的调度。<br />
这里不仅有技术层面的问题，还有业务的问题，如每次批量处理多少，间隔多少，是否需要失败重试等等？</p>
<h3>(3)消费</h3>
<p>最后，则是具体的计划任务执行，以完成必要的操作，也称为消费。<br />
很多传统的做法，都是把这些操作和接口混在一起的，而这里，PhalApi则会以一种更为明朗的方式来实现，从而自底而上，支持更多的调度方式和触发机制。  </p>
<h2>1.31.3 传统的计划任务</h2>
<p><img src="http://webtools.qiniudn.com/20150514225554.jpg" alt="a pic" />   </p>
<p>如果以一图而鳖之，上图虽然简化，但可以很好地说明传统计划任务的结构体系。<br />
即：很多项目都是使用内嵌的方式来包含计划任务，这样明显会把接口服务系统和后台计划任务混在一起，增加了系统间的耦合性。<br />
虽然小项目可以忍受或者适合这种混合，但是出于长远考虑，进行有意识地分解还是很有好处的。  </p>
<p>而且这种混合潜意识下又让开发人员不加判断就进行调用，这会严重增加接口的反应时间。<br />
我曾目睹一个接口耗时了近36秒之久，在对这个旧系统的接口进行一番排查后，原来是这个接口在发布后对上百个好友做了通知推送导致产生了上百条insert语句。  </p>
<h3>(1)传统的调度方式</h3>
<p>我们重点关注一下传统计划任务的调度方式，在过去，我们通常会有两种方式：一种是启动死循环的进程，另一种是启动一个crontab之类的定时任务。<br />
当然，上述的在接口请求时同步进行调度也算一种方式，但不是正规的做法。  </p>
<p>如果采用死循环的方式，我们还需要考虑代码更新升级后，对脚本的重启，以便载入新的代码。如果是sh循环调用PHP脚本，则可以忽略。</p>
<h2>1.31.4 新型的计划任务</h2>
<h3>(1)以接口的形式提供计划任务服务</h3>
<p>PhalApi中最具特色的做法是，将计划任务的执行消费实现，以接口形式来提供。<br />
这样的好处在于，我们作为接口开发人员，可以以熟悉的方式来进行计划任务的开发。<br />
但更大的得益在于，将计划任务通过接口的形式提供后，我们会看到更为广阔的使用场景：我们可以使用MQ队列消费，可以同步请求也可以异步请求。  </p>
<h3>(2)系统架构</h3>
<p>我们所做的，不仅仅只是把原来混合型的代码作简单分解，如下：
<img src="http://webtools.qiniudn.com/20150514225614.jpg" alt="a pic" />  </p>
<p>而是以一种更为正统的做法，为此我们添加了一些必要的节点来设计此构架。新的实现方式下的体系结构如下：<br />
<img src="http://webtools.qiniudn.com/20150514225619.jpg" alt="a pic" />  </p>
<h4>节点说明</h4>
<p>在上图中，应用节点还是我们的接口系统；MQ队列则是用于存放待消费的场景信息，同其他的MQ一样；计划任务则可以分为两部分，API接口实现和任务调度。<br />
计划任务这两部分，物理部署上可以合在一起，也可以分开，这取决于应用系统是采用分布式的做法，还是单一的服务器。  </p>
<h4>执行流程</h4>
<p>由上图可以看出，一个完整的计划任务流程为：</p>
<ul>
<li>1、应用产生一条新的计划任务，并存放于MQ队列</li>
<li>2、计划任务定时或者不停扫描新的计划任务；若有，则进行调度</li>
<li>3、计划任务API完成需要的工作，并将结果返回调度器</li>
</ul>
<h3>(3)单个添加，批量处理</h3>
<p>这里只支持单个MQ添加，而处理则是批量的，且每批处理的数据可指定配置。  </p>
<h3>(4)MQ共享</h3>
<p>无论是分布式还是本地一体化，MQ队列都应该是可以共享访问的，以便为应用节点、计划任务调度节点所访问，如下图所示：
<img src="http://webtools.qiniudn.com/20150514225632.jpg" alt="a pic" />   </p>
<h4>首选redis MQ</h4>
<p>因为MQ作为频繁读写的媒介，应该优先使用高效缓存来提高系统的吞吐率以及增加并发的能力。此外，作为临时一次性的数据，使用高效缓存也是大有好处的（但我们也需要考虑到数据丢失的情况）。<br />
而且，为了支持 <strong>单个添加，批量处理</strong>，第三方缓存应该很好地支持队列的操作。<br />
所以，redis是一个不错的选择。  </p>
<p>如下，是redis简单的队列操作：</p>
<pre><code class="language-javascript">$redis = new Redis();
$redis-&gt;connect('127.0.0.1', 6300);

$redis-&gt;lpush('test_key', 'www');
$redis-&gt;lpush('test_key', 'phalapi');
$redis-&gt;lpush('test_key', 'net');

echo $redis-&gt;lpop('test_key'), "\n";
echo $redis-&gt;lpop('test_key'), "\n";
echo $redis-&gt;lpop('test_key'), "\n";</code></pre>
<h4>数据库MQ</h4>
<p>如果考虑到redis扩展不好安装，或者应用喜欢使用数据库来存放MQ，也是可以的。只需要用SQL的一些基本的操作语句便可做到FIFO。</p>
<h4>文件MQ</h4>
<p>文件MQ也是一种方式，但很少使用。</p>
<h3>(5)更丰富的调度方式</h3>
<h4>接口同步调度</h4>
<p>虽然也是同步调度，但是我们将计划任务隔离后，便于日后发现此同步的计划任务影响到接口的响应时间时，可以及时轻松地切换到后台异步处理的方式。  </p>
<h4>回归传统的调度</h4>
<p>我们也可以沿用传统的做法，即使用死循环的脚本调度，或者crontab类的定时任务。  </p>
<h4>MQ队列消费</h4>
<p>既然我们以接口服务的形式提供计划任务的操作，那么可以把同一接口的调度放置到同一队列中进行维护和消费。</p>
<h4>接口异步调度</h4>
<p>当计划任务以接口服务提供后，我们可以使用另一种免MQ的做法，即使用接口的异步调度。如下：<br />
<img src="http://webtools.qiniudn.com/20150514225625.jpg" alt="a pic" />   </p>
<p>这样既可以避免死循环带来的性能负载问题，也可以避免定时任务带来的延时问题，可以说异步调度是一种折中完美的做法。<br />
但这也可能是一种不负责任或者不安全的做法，因为我们无法跟进异步计划任务的结果。 </p>
<h4>本地调度和远程调度</h4>
<p>本地调度是指在执行过程中构建模拟接口的调用而无须经过网络请求，远程调度则是通过远程接口请求来实现。<br />
如果把本地调度和远程调度，跟同步/异步组合起来，我们可以得到以下三种有意义的组合：</p>
<ul>
<li>本地同步调度</li>
<li>远程同步调度</li>
<li>远程异步调度</li>
</ul>
<h3>(6)计划任务的划分</h3>
<h4>service即类型</h4>
<p>明显地，接口服务名称service即可作为计划任务划分的依据。  </p>
<p>不同的service作为不同的队列，不同类型的计划任务；而相同的service则作为相同的队列相同的计划任务。  </p>
<h4>接口参数即参数</h4>
<p>接口参数即可计划任务执行时所需要的上下文信息。</p>
<h2>1.31.5 PhalApi中计划任务的核心设计解读</h2>
<h3>(1)桥接模式 - 数据与行为独立变化</h3>
<p>为了给计划任务一个执行的环境，我们提供了 <strong>计划任务调度器</strong> ，即：Task_Runner。<br />
每个计划任务需要调度的接口是不一样的，即不同的接口服务决定不同的行为；每个行为需要的数据也不一样，即不同的接口参数决定不同的数据。  </p>
<p>自然而言的，Task_Runner按照桥接模式，其充当的角色如下：<br />
<img src="http://webtools.qiniudn.com/task_1.jpg" alt="a pic" />  </p>
<p>然后，我们就可以这样各自实现：
<img src="http://webtools.qiniudn.com/task_2.jpg" alt="a pic" /> </p>
<h3>(2)适配器模式 - 对象适配器和类适配器</h3>
<p>在对MQ进行实现时，我们提供的Redis MQ队列、文件MQ队列和DB MQ队列，都使用了适配器模式，以重用框架已有的功能。<br />
其中，Redis MQ队列和文件MQ队列是属于对象适配器，DB MQ队列是类适配器。
对于对象适配器，我们也提供了外部注入，以便客户端在使用时可以轻松定制扩展，当然也可以使用默认的缓存。  </p>
<p>如下：
<img src="http://webtools.qiniudn.com/task_3.jpg" alt="a pic" /> </p>
<p>这样以后，我们可以这样根据创建不同的MQ队列：</p>
<pre><code class="language-javascript">//Redis MQ队列
$mq = Task_MQ_Redis();
//或
$mq = Task_MQ_Redis(new PhalApi_Cache_Redis(array('host' =&gt; '127.0.0.1', 'port' =&gt; 6379)));

//文件MQ队列
$mq = new Task_MQ_File();
//或
$mq = new Task_MQ_File(new PhalApi_Cache_File(array('path' =&gt; '/tmp/cache')));

//DB MQ队列
$mq = new Task_MQ_DB();

//Array MQ队列
$mq = new Task_MQ_Array();</code></pre>
<h3>(3)模板方法 - 本地和远程两种调度策略</h3>
<p>在完成底层的实现后，我们可以再来关注如何调度的问题，目前可以有本地调度和远程调度两种方式。  </p>
<ul>
<li>本地调度：是指本地模拟接口的请求，以实现接口的调度</li>
<li>远程调度：是指通过计划任务充当接口客户端，通过请求远程服务器的接口以完成接口的调度</li>
</ul>
<p>为此，我们的设计演进成了这样：
<img src="http://webtools.qiniudn.com/task_4.jpg" alt="a pic" />  </p>
<p>上图多了两个调度器的实现类，并且远程调度器会将远程的接口请求功能委托给连接器来完成。  </p>
<h3>(4)设计审视</h3>
<p>好了！让我们再回头审视这样的设计。  </p>
<p>首先，我们在高层，也就是规约层得到了很好的约定。<br />
不必过多地深入理解计划任务内部的实现细节，我们也可以轻松得到这样的概念流程：<br />
<strong> 计划任务调度器(Task_Runner)从MQ队列(Task_MQ)中不断取出计划任务接口服务(PhalApi_Api)进行消费。</strong>  </p>
<p>再下一层，则是具体的实现，即我们所说的实现层。<br />
客户可以根据自己的需要进行选取使用，他们也可以扩展他们需要的MQ。重要的是，他们需要自己实现计划任务的接口服务。  </p>
<p>根据爱因斯坦说的，要保持简单，但不要过于简单。<br />
所以，为了更好地理解计划任务的运行过程，我们提供了简单的时序图：<br />
<img src="http://webtools.qiniudn.com/task_5.jpg" alt="a pic" />  </p>
<p>上图主要体现了两个操作流程：加入MQ和MQ消费。<br />
其中，注意这两个流程是共享同一个MQ的，否则不能共享数据。同时调度是会进行循环式的调度，并且穷极之。</p>
<h3>(5)没有引入工厂方法的原因</h3>
<p>我们在考虑是否需要提供工厂方法来创建计划任务调度器，或者MQ。<br />
但我们发现，设计是如此明了，不必要再引入工厂方法来增加使用的复杂性，因为存在组合的情况。而且，对于后期客户端进行扩展也不利。  </p>
<p>当我们需要启动一个计划任务时，可以这样写：</p>
<pre><code class="language-javascript">$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);

$runner-&gt;go('MyTask.DoSth');</code></pre>
<p>上面简单的组合可以有：4种MQ * 2种调度 = 8种组合。  </p>
<p>所以，我们最后决定不使用工厂方法，而是把这种自由组合的权利交给客户端。  </p>
<h3>(6)失败重试与并发问题</h3>
<p>除了对计划任务使用什么模式进行探讨外，我们还需要关注计划任务其他运行时的问题。  </p>
<p>一个考虑的是失败重试，这一点会发生在远程调度中，因为接口请求可能会超时。这时我们采用的是失败轮循重试。<br />
即，把失败的任务放到MQ的最后，等待下一批次的尝试。连接器在进行请求时，也会进行一定次数的超时重试。这里主要是为了预防接口服务器崩溃后的计划任务丢失。  </p>
<p>另一个则是并发的问题。这里并没有过多地进行加锁策略。<br />
而是把这种需要的实现移交给了客户端。因为加锁会使得计划任务更为复杂，而且有时不一定需要使用，如一个计划任务只有一个进程时，也就是单个死循环的脚本进程的情况。  </p>
<h3>(7)客户端的使用</h3>
<p>最后，客户端的使用就很简单了：</p>
<pre><code class="language-javascript">$mq = new Task_MQ_Redis();
$taskLite = new Task_Lite();

$taskLite-&gt;add('MyTask.DoSth', array('id' =&gt; 888));</code></pre>
<hr />
<h4><a href="/wikis/%5B1.30%5D-%E4%BF%9D%E6%8C%81%E7%9A%84%E5%8A%9B%E9%87%8F%EF%BC%9A%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.11%5D-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%EF%BC%9ADI%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5-%E8%AE%A9%E8%B5%84%E6%BA%90%E6%9B%B4%E5%8F%AF%E6%8E%A7.html">下一章</a>#[2.11]-核心思想：DI依赖注入-让资源更可控</h4>
<h4><a href="/wikis/%5B1.32%5D-%E5%9C%A8%E7%BA%BF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%EF%BC%9A%E6%B3%A8%E9%87%8A%E8%A7%84%E8%8C%83.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.12%5D-%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%EF%BC%9A%E5%8F%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html">下一章</a></h4>
<hr />
<p><em>一个没有绝对答案的世界，却拥有绝对的丰富。  --《沈奇岚：我愿生命从容》</em>  </p>
<h2>2.11.1 定义</h2>
<h3>(1) 关于依赖注入</h3>
<p>即控制反转，目的是了减少耦合性，简单来说就是使用开放式来获取需要的资源。</p>
<h3>(2) 关于资源</h3>
<p>这里说的资源主要是在开发过程中使用到的资源，包括配置项；数据库连接、Memcache、接口请求等系统级的服务；以及业务级使用到的实例等。</p>
<p>引入依赖注入的目的不仅是为了增加一个类，而是为了更好的对资源进行初始化、管理和维护。下面将进行详细的说明。  </p>
<h2>2.11.2 一个简单的例子</h2>
<p>很多时候，类之间会存在依赖、引用、委托的关系，如A依赖B时，可以这样使用：</p>
<pre><code class="language-javascript">class A {
    protected $_b;

    public function __construct()
    {
        $this-&gt;b = new B();      
    }
}</code></pre>
<p>这种方式在A内限制约束了B的实例对象，当改用B的子类或者改变B的构建方式时，A需要作出调整。这时可以通过依赖来改善这种关系：</p>
<pre><code class="language-javascript">class A {
    protected $_b;

    public function __construct($b)
    {
        $this-&gt;b = $b;       
    }
}</code></pre>
<p>再进一步，可以使用DI对B的对象进行管理：</p>
<pre><code class="language-javascript">class A {
    public function __construct()
    {       
    }

    public function doSth()
    {
        //当你需要使用B时
        $b = $di-&gt;get('B');
    }
}</code></pre>
<p>这样的好处？  </p>
<p>一方面，对于使用A的客户（指开发人员），不需要再添加一个B的成员变量，特别不是全部类的成员函数都需要使用B类服务时。另一方面在外部多次初始化A实例时，可以统一对B的构建。</p>
<h2>2.11.3 依赖注入的使用示例</h2>
<p>为方便使用，调用的方式有：set/get函数、魔法方法setX/getX、类变量$fdi-&gt;X、数组$fdi['X']，初始化的途径有：直接赋值、类名、匿名函数。</p>
<pre><code class="language-javascript">/** ------------------ 创建与设置 ------------------ **/
//获取DI
$di = DI();
//演示的key
$key = 'demoKey';

/** ------------------ 设置 ------------------ **/
//可赋值的类型：直接赋值、类名赋值、匿名函数
$di-&gt;set($key, 'Hello DI!');
$di-&gt;set($key, 'Simple');
$di-&gt;set($key, function(){
    return new Simple();
});
//设置途径：除了上面的set()，你还可以这样赋值
$di-&gt;setDemoKey('Hello DI!');
$di-&gt;demoKey = 'Hello DI!';
$di['demoKey'] = 'Hello DI!';

/** ------------------ 获取 ------------------ **/
//你可以这样取值
echo $di-&gt;get('demoKey'), "\n";
echo $di-&gt;getDemoKey(), "\n";
echo $di-&gt;demoKey, "\n";
echo $di['demoKey']. "\n";

/**
 * 演示类
 */
class Simple
{
    public function __construct()
    {
    }
}</code></pre>
<h2>2.11.4 依赖注入的好处</h2>
<h3>(1)减少对各个类编写工厂方法以单例获取的开发量</h3>
<p>DI相当于一个容器，里面可以放置基本的变量，也可以放置某类服务，甚至是像文件句柄这些的资源。在这容器里面，各个被注册的资源只会存在一份，也就是当被注册的资源为一个实例对象时，其效果就等于单例模式。  </p>
<p>因此，保存在DI里面的类，不需要再编写获取单例的代码，直接通过DI获取即可。  </p>
<p>例如很多API的服务组件以及其他的一些类，都实现了单例获取的方式。分别如：  </p>
<p>微博接口调用：  </p>
<pre><code class="language-javascript">&lt;?php
class Weibo_Api
{
    protected static $_instance = null;

    public static function getInstance()
    {
        if (!isset(self::$_instance)) {
            self::$_instance = new Weibo_Api();
        }
        return self::$_instance;
    }

    //....
}</code></pre>
<p>七牛云存储接口调用：</p>
<pre><code class="language-javascript">class Qiniu_Api {
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new Qiniu_Api();
        }
        return self::$_instance;
    }
}</code></pre>
<p>QQ开放平台接口调用：</p>
<pre><code class="language-javascript">class QQ_Api { 
    private static $_instance = null; //实例对象

    public static function getInstance()
    {
        if (self::$_instance ===null) {
            self::$_instance = new QQ_Api();
        }
        return self::$_instance;
    }
}</code></pre>
<p>如果使用DI对上面这些服务进行管理，则上面三个类乃至其他的类对于单例这块的代码都可以忽略不写。注册代码如下：</p>
<pre><code class="language-javascript">$di-&gt;sStockApi = 'Weibo_Api';
$di-&gt;sDioAopi = 'Qiniu_Api';
$di-&gt;sShopApi = 'QQ_Api';</code></pre>
<p>上面是通过类名来进行延迟加载，但需要各个类提供public的无参数的构造函数。如果各个服务需要进行初始化，可以将初始化的工作放置在onInitialize()函数内，DI在对类实例化时会回调此函数进行初始化。</p>
<h3>(2)统一资源注册，便于后期维护管理</h3>
<p>这里引入DI，更多是为了“一处创建，多处使用”， 而不是各自创建，各自使用。</p>
<h4>创建和使用分离</h4>
<p>考虑以下场景：假设有这样的业务数据需要缓存机制，所以可注册一个实现缓存机制的实例：</p>
<pre><code class="language-javascript">$di-&gt;set('cache', new FileCache());</code></pre>
<p>然后提供给多个客户端使用：</p>
<pre><code class="language-javascript">$di['cache']-&gt;set('indexHtml', $indexContent);   //缓存页面
$di['cache']-&gt;set('config', $config);  //缓存公共配置
$di['cache']-&gt;set('artistList', $artistList);   //缓存数据</code></pre>
<p>当需要切换到MC或者Redis缓存或者多层缓存时，只需要修改对缓存机制的注入即可，如：</p>
<pre><code class="language-javascript">$di-&gt;set('cache', new RedisCache());</code></pre>
<p>依赖注入的一个很大的优势就在于可以推迟决策，当需要用到某个对象时，才对其实例化。可以让开发人员在一开始时不必要关注过多的细节实现，同时也给后期的扩展和维护带来极大的方便。  </p>
<p>再上一层，假设未来我们需要更高级的缓存服务，那么我们可以在不影响客户端使用的情况下，轻松升级。<br />
未来的可配置化的多级缓存策略</p>
<p>以下是一个模拟的使用场景，但依然对现在的项目有一定的帮助。假设我们现在有一个MC集群的缓存且引入了DI，使用如下：   </p>
<pre><code class="language-javascript">&lt;?php

//初始化
$di = PhalApi_DI::one();
$di-&gt;cache = new Memcache();
$di-&gt;cache-&gt;connect('localhost', 11211);

//不同文件的多处使用 ...
echo $di-&gt;cache-&gt;get('key');
echo $di-&gt;cache-&gt;get('key2');
echo $di-&gt;cache-&gt;get('key3');
...</code></pre>
<p>假设现在发现一层缓存存在穿透情况，为保证服务器的稳定性，我们已开发实现了多层缓存策略，并且可以通过简单配置即可实现，只需要对DI容器里面的cache实例进行升级，其他客户端的调用即可马上享受到缓存升级的优质服务。升级涉及改动的代码如下：</p>
<pre><code class="language-javascript">&lt;?php

//初始化
$di = new PhalApi_DI();
$di-&gt;cache = function () {
$ultraFastFrontend = new DataFrontend(array(
    "lifetime" =&gt; 3600
));

$fastFrontend = new DataFrontend(array(
    "lifetime" =&gt; 86400
));

$slowFrontend = new DataFrontend(array(
    "lifetime" =&gt; 604800
));

return new Multiple(array(
    new ApcCache($ultraFastFrontend, array(
        "prefix" =&gt; 'cache',
    )),
    new MemcacheCache($fastFrontend, array(
        "prefix" =&gt; 'cache',
        "host" =&gt; "localhost",
        "port" =&gt; "11211"
    )),
    new FileCache($slowFrontend, array(
        "prefix" =&gt; 'cache',
        "cacheDir" =&gt; "../app/cache/"
    ))
));
};</code></pre>
<p>备注：关于多级缓存策略，后续会提供源代码和重用库，或者期待读者的分享。</p>
<h3>(3)延迟式加载，提高性能</h3>
<p>延迟加载可以通过DI中的类名初始化、匿名函数和参数配置（未实现）三种方式来实现。  </p>
<p>延迟加载有时候是非常有必要的，如在初始化项目的配置时，随着配置项的数据增加，服务器的性能也将逐渐受到影响，因为配置的内容可能是硬编码，可能来自于数据库，甚至需要通过接口从后台调用获取， 特别当很多配置项不需要使用时。而此时，支持延时加载将可以达到很好的优化，而不用担心在需要使用的时候忘记了初始化。从而很好的提高服务器性能，提高响应速度。  </p>
<p>如对一些耗时的资源先进行匿名函数的初始化：   </p>
<pre><code>$di['hightResource'] = function() {
    //获取返回耗性能的资源
    //return $resource; 
}</code></pre>
<h3>(4)以优雅的方式取代滥用的全局变量</h3>
<p>在我看来，PHP里面是不应该使用全局变量（global和$_GLOBALS），更不应该到处使用。  </p>
<p>用了DI来管理，即可这样注册：</p>
<pre><code class="language-javascript">$di-&gt;set('debug', true);</code></pre>
<p>然后这样使用：</p>
<pre><code class="language-javascript">$debug = $di-&gt;get('debug');</code></pre>
<p>也许有人会想：仅仅是换个地方存放变量而已吗？其实是换一种思想使用资源。  </p>
<p>以此延伸，DI还可用于改善优化另外两个地方：通过include文件途径对变量的使用和变量的多层传递。  </p>
<p>变量的多层传递，通俗来说就是漂洋过海的变量。  </p>
<h2>2.11.5 DI思想的来源与推荐参考</h2>
<p><a href="http://docs.phalconphp.com/en/latest/reference/di.html">Dependency Injection/Service Location</a></p>
<hr />
<h4><a href="/wikis/%5B1.32%5D-%E5%9C%A8%E7%BA%BF%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%EF%BC%9A%E6%B3%A8%E9%87%8A%E8%A7%84%E8%8C%83.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.12%5D-%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%EF%BC%9A%E5%8F%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html">下一章</a>#[2.12]-海量数据：可配置的分库分表</h4>
<h4><a href="/wikis/%5B2.11%5D-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%EF%BC%9ADI%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5-%E8%AE%A9%E8%B5%84%E6%BA%90%E6%9B%B4%E5%8F%AF%E6%8E%A7.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.13%5D-%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95%EF%BC%9A%E5%9C%A8%E7%BA%BFSQL%E8%AF%AD%E5%8F%A5%E6%9F%A5%E7%9C%8B%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html">下一章</a></h4>
<hr />
<h2>2.12.1 背景</h2>
<p>为了应对产品海量用户的愿景需求，这里将设计一个分布式的数据库存储方案，以便能满足数据量的骤增、云服务的横向扩展、后台接口开发的兼容性，以及数据迁移等问题，避免日后因为全部数据都存放在单台服务器上的限制。</p>
<h2>2.12.2 主要思想</h2>
<ul>
<li>1、分库分表</li>
<li>2、路由规则</li>
<li>3、扩展字段</li>
<li>4、可配置</li>
<li>5、SQL语句自动生成</li>
</ul>
<h3>(1)分库分表</h3>
<p>是指将不需要进行必要关联查询的表分开存放，如存放事件推送的weili_event_pushto和存放标签的weili_tag；同时，对于 同一个表，因为存放的数据量是可预见式的暴增，如上述的weili_event_pushto，每时每刻都会产生大量的来自用户发布的事件，因此为了突破 MySQL单表的限制以及其他问题，需要将此表同时创建N份。</p>
<h3>(2)路由规则</h3>
<p>在上面进行了分库分表后，开发人员在读取时，就需要根据相应的规则找到对应 的数据库和数据库表，这里建议每个表都需要有int(11)类型的id字段，以便作为分表的参考。</p>
<h3>(3)扩展字段</h3>
<p>在完成了分库分表和制定路由规则后，考虑到日后有数据库的DB变更，为减少DB变更对现有数据库表的影响，这里建议每个表都增加text类型的extra_data字段，并且使用json格式进行转换存储。</p>
<h3>(4)可配置</h3>
<p>在有了N台数据库服务器以及每个表都拆分成M张表后，为减少后台接口开发人员的压力，有必须在后台接口框架提供可配置 的支持。即：数据库的变更不应影响开发人员现有的开发，也不需要开发人员作出代码层面的改动，只需要稍微配置一下即可。关于这块，请见下面的框架实现部 分。</p>
<h3>(5)SQL语句自动生成</h3>
<p>对于相同表的建表语句，可以通过脚本来自动生成，然后直接导入数据即可。  </p>
<h2>2.12.3 PhalApi框架的实现方案</h2>
<p>PhalApi框架主要需要实现的是路由这一层的映射，并且通过可配置的方式进行控制，同时还应支持生产环境和测试环境的异同，如在测试环境我们明显不需要1000张数据库的表。为此，需要提供一种 <strong>表名 + id</strong> 映射到 <strong>数据库服务器 + 具体哪张表</strong> 的规则。
<img src="http://webtools.qiniudn.com/20150411005257_8ea80ab617023e2f377bf1ba8fdff6c5" alt="show" />
如上图所示，表名会统一加上前缀，并且将id按一定的表总数进行取模，最后再根据得到的具体表名，通过映射表查找到对应 的数据库服务器进行操作。其中，model层为开发实现，数据库表的映射由接口框架实现支持。</p>
<h2>2.12.4 使用示例</h2>
<h3>(1)配置数据库的路由配置</h3>
<p>修改./Config/dbs.php文件，以下是参考的示例配置。其中servers为DB服务器，包括数据库的账号信息等，tables为数据库表的映射关系，其中<strong>default</strong>下标为缺省的数据库路由。  </p>
<p>在每个数据库表里面，可以配置多个数据库表，通过开始的下标start和结束的下标end来对表进行分布式存放，并且如果没有start和end的，则视为不需要拆分存放，同时也是当找不到合适时的拆分表时所采用的默认配置。</p>
<pre><code class="language-javascript">return array(
    /**
     * avaiable db servers
     */
    'servers' =&gt; array(
        'db_demo' =&gt; array(
            'host'      =&gt; 'localhost',             //数据库域名
            'name'      =&gt; 'test',                  //数据库名字
            'user'      =&gt; 'root',                  //数据库用户名
            'password'  =&gt; '123456',                //数据库密码
            'port'      =&gt; '3306',                  //数据库端口
        ),
    ),

    /**
     * custom table map
     */
    'tables' =&gt; array(
        '__default__' =&gt; array(
            'prefix' =&gt; 'tbl_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
            ),
        ),
        'demo' =&gt; array(
            'prefix' =&gt; 'tbl_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
                array('start' =&gt; 0, 'end' =&gt; 2, 'db' =&gt; 'db_demo'),
            ),
        ),
    ),
);</code></pre>
<p>上面示例配置的意思是：</p>
<pre><code class="language-javascript">表名   DB服务器
  tbl_demo    db_demo
  tbl_demo_0      db_demo
  tbl_demo_1      db_demo
  tbl_demo_2      db_demo</code></pre>
<h3>(2)准备需要创建表的基本SQL语句</h3>
<p>这里说的基本SQL语句是指：仅是这个表所特有的字段，排除已固定公共有的自增主键id，和扩展字段ext_data。下面是一个示例：</p>
<pre><code class="language-javascript">`name` varchar(11) DEFAULT NULL,</code></pre>
<h3>(3)生成并导入SQL语句</h3>
<p>由于拆分后的数据库表数量众多，这里提供了一个快捷的脚本工具来生成所需要创建的数据库表。</p>
<pre><code class="language-javascript">$ php ./build_sqls.php
Usage: ./build_sqls.php &lt;table&gt; [engine=InnoDB]</code></pre>
<p>执行上面的脚本，输入数据库表参数后：</p>
<pre><code class="language-javascript"> php ./build_sqls.php demo</code></pre>
<p>将会从配置文件 里面寻找所需要创建的表，并生成类似以下的SQL语句：</p>
<pre><code class="language-javascript">/**
 * DB: localhost  db_demo
 */

CREATE TABLE `demo` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/**
 * DB: localhost  db_demo
 */

CREATE TABLE `tpl_demo_0` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_1` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `tpl_demo_2` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(11) DEFAULT NULL,
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre>
<h3>(4)使用与代码开发</h3>
<p>在将上面的SQL语句导入数据库后，即可以像之前那样操作数据库。下面是一些示例：</p>
<pre><code class="language-javascript">DI()-&gt;notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()-&gt;config-&gt;get('dbs'), true);

DI()-&gt;notorm-&gt;demo-&gt;where('id', '1')-&gt;fetch();</code></pre>
<p>用到了拆分表的代码示例，假设event表被拆分成了3个表，则客户端在调用里，需要根据（id % 3 ）来拼接合适的数据库表名，其他使用不变。</p>
<pre><code class="language-javascript">DI()-&gt;notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()-&gt;config-&gt;get('dbs'), true);

$row = DI()-&gt;notorm-&gt;demo_0-&gt;where('id', '3')-&gt;fetch();
$row = DI()-&gt;notorm-&gt;demo_1-&gt;where('id', '10')-&gt;fetch();
$row = DI()-&gt;notorm-&gt;demo_2-&gt;where('id', '2')-&gt;fetch();</code></pre>
<h4>使用Model基类的情况</h4>
<p>更好的写法，应该是继承于PhalApi_Model_NotORM，并统一实现分表的操作，如：  </p>
<pre><code class="language-javascript">&lt;?php

class Model_Demo extends PhalApi_Model_NotORM {

    protected function getTableName($id) {
        $tableName = 'demo';
        if ($id !== null) {
            $tableName .= '_' . ($id % 3);
        }
        return $tableName;
    }
}</code></pre>
<p>然后，上面的查询分别对应：  </p>
<pre><code class="language-javascript">$model = new Model_Demo();

$row = $model-&gt;get('3', 'id');
$row = $model-&gt;get('10', 'id');
$row = $model-&gt;get('2', 'id');</code></pre>
<p>更进一步，我们可以通过$this-&gt;getORM($id)来获取分表的实例进行分表的操作，如：  </p>
<pre><code class="language-javascript">&lt;?php

class Model_Demo extends PhalApi_Model_NotORM {

    //... ...

    public function getNameById($id) {
        $row = $this-&gt;getORM($id)-&gt;select('name')-&gt;fetchRow(); //假设$id为3，则 $this-&gt;getORM($id) 等效于 DI()-&gt;notorm-&gt;demo_0
    return !empty($row) ? $row['name'] : '';
    }

}</code></pre>
<h2>2.12.5 多个数据库的配置方式</h2>
<p>当需要使用多个数据库时，可以先在servers中可以配置多组数据库的信息，然后在tables为不同的数据库表指定不同的数据库服务器。  </p>
<p>假设我们有两台数据库服务器，分别叫做db_A、db_B，即：  </p>
<pre><code class="language-javascript">return array(
    /**
     * DB数据库服务器集群
     */
    'servers' =&gt; array(
        'db_A' =&gt; array(                              //db_A
            'host'      =&gt; '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' =&gt; array(                              //db_B
            'host'      =&gt; '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    //... ...</code></pre>
<p>若db_A服务器中的数据库有表a_table_user、a_table_friends，而db_B服务器中的数据库有表b_table_article、b_table_comments，则：  </p>
<pre><code class="language-javascript">&lt;?php
return array(

    //... ...

    /**
     * 自定义路由表
     */
    'tables' =&gt; array(
        //通用路由
        '__default__' =&gt; array(
            'prefix' =&gt; 'a_',  //以 a_ 为表前缀
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_article' =&gt; array(                                     //表b_table_article
            'prefix' =&gt; 'b_',                                         //表名前缀
            'key' =&gt; 'id',                                            //表主键名
            'map' =&gt; array(                                           //表路由配置
                array('db' =&gt; 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' =&gt; array(                                    //表b_table_article
            'prefix' =&gt; 'b_',                                         //表名前缀
            'key' =&gt; 'id',                                            //表主键名
            'map' =&gt; array(                                           //表路由配置
                array('db' =&gt; 'db_B'),                                // b_table_comments表使用db_B数据库
            ),
        ),
    ),
</code></pre>
<p>如果项目存在分表的情况，可结合上述的分表的说明进行配置。<br />
这里为了让大家更为明了，假设db_A服务器中的数据库有表a_table_user、a_table_friends_0到a_table_friends_9（共10张表），
而db_B服务器中的数据库有表b_table_article、b_table_comments_0到b_table_comments_19（共20张表），则结合起来的完整配置为：</p>
<pre><code class="language-javascript">&lt;?php
return array(
    /**
     * DB数据库服务器集群
     */
    'servers' =&gt; array(
        'db_A' =&gt; array(                              //db_A
            'host'      =&gt; '192.168.0.1',             //数据库域名
            // ... ...
        ),
        'db_B' =&gt; array(                              //db_B
            'host'      =&gt; '192.168.0.2',             //数据库域名
            // ... ...
        ),
    ),

    /**
     * 自定义路由表
     */
    'tables' =&gt; array(
        //通用路由
        '__default__' =&gt; array(
            'prefix' =&gt; 'a_',  //以 a_ 为表前缀
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_A'),  //默认，使用db_A数据库
            ),
        ),

        'table_friends' =&gt; array(                                     //分表配置
            'prefix' =&gt; 'a_',                                         //表名前缀
            'key' =&gt; 'id',                                            //表主键名
            'map' =&gt; array(                                           //表路由配置
                array('db' =&gt; 'db_A'),                                // b_table_comments表使用db_B数据库
                array('start' =&gt; 0, 'end' =&gt; 9, 'db' =&gt; 'db_A'),      //分表配置（共10张表）
            ),
        ),

        'table_article' =&gt; array(                                     //表b_table_article
            'prefix' =&gt; 'b_',                                         //表名前缀
            'key' =&gt; 'id',                                            //表主键名
            'map' =&gt; array(                                           //表路由配置
                array('db' =&gt; 'db_B'),                                // b_table_article表使用db_B数据库
            ),
        ),

        'table_comments' =&gt; array(                                    //表b_table_article
            'prefix' =&gt; 'b_',                                         //表名前缀
            'key' =&gt; 'id',                                            //表主键名
            'map' =&gt; array(                                           //表路由配置
                array('db' =&gt; 'db_B'),                                // b_table_comments表使用db_B数据库
                array('start' =&gt; 0, 'end' =&gt; 19, 'db' =&gt; 'db_B'),     //分表配置（共20张表）
            ),
        ),
    ),
);</code></pre>
<h2>2.12.6 与主从数据库的有机结合</h2>
<p>虽然这是专门为海量数据设计的存储方案，但也是可以结合主从配置来获得更庞大强壮的方案，当然为之付出的是复杂性的引入。  </p>
<p>简单地，可以将dbs.php复制一份dbs_slave.php出来给从库使用，然后注册一个从库的服务：</p>
<pre><code class="language-javascript">DI()-&gt;slaveNotorm = new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('slave_dbs'));</code></pre>
<p>最后，在需要使用从库来读取时，使用slaveNotorm 服务即可。</p>
<h2>2.12.7 不足与注意点</h2>
<p>这样的设计是有明显的灵活性的，因为在后期如果需要迁移数据库服务器，我们可以在框架支持的情况下轻松应对，但依然需要考虑到一些问题和不足。</p>
<h3>(1)DB变更</h3>
<p>DB变更，这块是必不可少的，但一旦数据库表被拆分后，表数量的骤增导致变更执行困难，所以这里暂时使用了一个折中的方案，即提供了一个ext_data 扩展字段用于存放后期可能需要的字段信息，建议采用json格式，因为通用且长度比序列化的短。但各开发可以根据自己的需要决定格式。即使如此，扩展字段 明显做不到一些SQL的查询及其他操作。</p>
<h3>(2)表之间的关联查询</h3>
<p>表之间的关联查询，这个是分拆后的最大问题。虽然这样的代价是我们可以得到更庞大的存储设计， 而且很多表之间不需要必须的关联的查询，即使我们需要，我们也可以通过其他手段如缓存和分开查询来实现。这对开发人员有一定的约束，但是对于可预见性的海 量数量，这又是必须的。</p>
<hr />
<h4><a href="/wikis/%5B2.11%5D-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%EF%BC%9ADI%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5-%E8%AE%A9%E8%B5%84%E6%BA%90%E6%9B%B4%E5%8F%AF%E6%8E%A7.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.13%5D-%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95%EF%BC%9A%E5%9C%A8%E7%BA%BFSQL%E8%AF%AD%E5%8F%A5%E6%9F%A5%E7%9C%8B%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html">下一章</a></h4>
<h1>[2.13]-接口调试：在线SQL语句查看与性能优化</h1>
<h4><a href="/wikis/%5B2.12%5D-%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%EF%BC%9A%E5%8F%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.14%5D-%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%EF%BC%9A%E6%84%8F%E5%9B%BE%E5%AF%BC%E5%90%91%E7%BC%96%E7%A8%8B%E4%B8%8B%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91.html">下一章</a></h4>
<hr />
<p>后台接口绝大数情况下，都需要与数据库进行交互，以获取业务数据或者接收保存客户端上报的数据。为方便后台开发同学进行调试，以及实时查看全部执行的SQL语句，这里简单地对全部执行的SQL语句进行调试模式下输出。</p>
<h2>2.13.1 开启SQL调试</h2>
<p>开启调试模式很简单，但这里和通常的框架不一样，我们不是全部统一地开启调试模式，因为在接口正常调用情况下返回非法的JSON会导致接口结果解析失败。故我们通过添加调试参数来控制是否开启SQL调试。如下：</p>
<pre><code class="language-javascript">//$vim ./Public/init.php

//数据操作 - 基于NotORM
DI()-&gt;notorm = function() {
    $debug = isset($_GET['debug']) ? true : false;
    return new PhalApi_DB_NotORM(DI()-&gt;config-&gt;get('dbs'), $debug);
};</code></pre>
<blockquote>
<p>特别注意：
通常，我们的调试参数不应都简单地使用&amp;debug=1，而是各自定义，如  <strong>复杂一点：&amp;__phalapi_debug__=1</strong> ，或者再添加一个简单的验签，额外带个参数校验，如：&amp;<strong>phalapi</strong>sign__=202cb962ac59075b964b07152d234b70。减少暴露SQL的风险。</p>
</blockquote>
<h2>2.13.2 调试示例</h2>
<p>回到前面获取用户基本信息接口 /demo/?service=User.GetBaseInfo 的示例。</p>
<h3>(1)正常情况下</h3>
<p>请求：  </p>
<pre><code class="language-javascript">http://dev.phalapi.com/demo/?service=User.GetBaseInfo&amp;user_id=1</code></pre>
<p>返回： </p>
<pre><code class="language-javascript">{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}</code></pre>
<h3>(2)带&amp;debug=1调试下</h3>
<p>请求：</p>
<pre><code>http://dev.phalapi.com/demo/?service=User.GetBaseInfo&amp;user_id=1&amp;debug=1</code></pre>
<p>返回：</p>
<pre><code class="language-javascript">[1 - 0.00057s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
{"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","note":"oschina"}},"msg":""}</code></pre>
<h2>2.13.3 一个错误的接口开发</h2>
<p>有时，在进行接口开发时，会需要进行批量获取的功能，如列表。但很多开发的同学可能会因为时间赶或者没有意识去对SQL查询进行优化，或者甚至不知道自己的接口背后隐藏着多少问题。下面是一个错误的开发示例。  </p>
<h3>(1)新增的批量获取接口</h3>
<p>假设我们在开发一个国际的项目，并且运行良好，BOSS说因业务需要，要加多一个接口以支持批量获取用户的基本信息，提供给国外某知名的社交平台调用。</p>
<p>于是乎，我们很快就根据原来的单个获取接口实现了新的接口：</p>
<pre><code class="language-javascript">//$vim ./Demo/Api/User.php
&lt;?php

class Api_User extends PhalApi_Api {

    public function getRules() {
        return array(
            //...
            'getMultiBaseInfo' =&gt; array(
                'user_ids' =&gt; array('name' =&gt; 'user_ids', 'type' =&gt; 'array', 'format' =&gt; 'explode', 'require' =&gt; true),
            ),
        );
    }

    //...

    public function getMultiBaseInfo() {
        $rs = array('code' =&gt; 0, 'msg' =&gt; '', 'list' =&gt; array());

        $domain = new Domain_User();
        foreach ($this-&gt;user_ids as $userId) {
            $rs['list'][] = $domain-&gt;getBaseInfo($userId);
        }

        return $rs;
    }
}
</code></pre>
<h3>(2)运行调用一下</h3>
<p>显然，我们可以很清楚地调用新增的接口：</p>
<pre><code>http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&amp;user_ids=1,2,3</code></pre>
<p>可返回：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,
        "msg": "",
        "list": [
            {
                "id": "1",
                "name": "dogstar",
                "note": "oschina"
            },
            {
                "id": "2",
                "name": "Tom",
                "note": "USA"
            },
            {
                "id": "3",
                "name": "King",
                "note": "game"
            }
        ]
    },
    "msg": ""
}</code></pre>
<p>假设我们已经有了这样的数据库表数据：</p>
<pre><code class="language-javascript">INSERT INTO `tbl_user` VALUES ('1', 'dogstar', 'oschina');
INSERT INTO `tbl_user` VALUES ('2', 'Tom', 'USA');
INSERT INTO `tbl_user` VALUES ('3', 'King', 'game');</code></pre>
<h3>(3)这样的问题？</h3>
<p>这样的问题，在对外黑盒调用的客户端同学是发现不了的，对于测试人员来说也是无法感知的。但所犯的错误也是显然易见的，就是没有进行SQL的批量查询优化，造成了很多不必要的重复查询。<br />
这里，根据后台接口开发人员提供的调试参数（假设为：&amp;debug=1），则我们可以快速发现存在的问题：</p>
<pre><code>http://dev.phalapi.com/demo/?service=User.GetMultiBaseInfo&amp;user_ids=1,2,3&amp;debug=1</code></pre>
<p>如下返回，我们看到了很多重复类似的查询语句。</p>
<pre><code class="language-javascript">[1 - 0.0005s]SELECT * FROM tbl_user WHERE (id = ?); -- 1
[2 - 0.00042s]SELECT * FROM tbl_user WHERE (id = ?); -- 2
[3 - 0.00038s]SELECT * FROM tbl_user WHERE (id = ?); -- 3
{"ret":200,"data":{"code":0,"msg":"","list":[{"id":"1","name":"dogstar","note":"oschina"},{"id":"2","name":"Tom","note":"USA"},{"id":"3","name":"King","note":"game"}]},"msg":""}</code></pre>
<p>上面输出的调试信息，简单补充一个格式：</p>
<pre><code class="language-javascript">[序号 - 所耗时间]SQl语句 -- [参数1, 参数2]</code></pre>
<h3>(4)如何改进？</h3>
<p>这是一个很基本的问题，当然在实际项目中不会普通存在，这里只是作为一个示例加以说明。但让人失望的是，实际项目确实存在为数不少的这样的情况。可能是新人的技术和意识问题，也有可能是老同学的态度问题。所以，优化这么一个接口的批量SQL查询不难，难的是如何才能让新、老同学都注重这块的SQL查询优化呢？而不是等到线上服务器异常崩溃后再来推托责任。<br />
具体的代码改进，留给读者自己实践了。毕竟，看了，实践了，才会真正深刻地掌握。</p>
<h2>2.13.4 由此引申</h2>
<ul>
<li>这里不专门讲述SQL的优化，但也顺便提供一些SQL查询优化的建议：</li>
<li>使用批量查询，而不是N次循环查询！</li>
<li>重复的数据，不要重复获取；</li>
<li>根据需要，按需要获取表字段，而不是SELECT *；</li>
<li>针对频繁的搜索字段，建立必要的索引，以加快查询速度；</li>
<li>使用关联查询，而不是粗暴地类似：where uid IN (... 这里是成千上W个用户ID ...)；</li>
<li>针对单条SQL语句执行时间超过1秒的，重点优化；</li>
</ul>
<h2>2.13.5 最后最后</h2>
<p>奉上我们坚持TDD开发下的单元测试代码：</p>
<pre><code class="language-javascript">    public function testGetMultiBaseInfo()
    {
        $str = 'service=User.GetMultiBaseInfo&amp;user_ids=1,2,3';
        parse_str($str, $params);

        DI()-&gt;request = new PhalApi_Request($params);

        $api = new Api_User();
        //自己进行初始化
        $api-&gt;init();
        $rs = $api-&gt;getMultiBaseInfo();

        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('msg', $rs);
        $this-&gt;assertArrayHasKey('list', $rs);

        foreach ($rs['list'] as $item) {
            $this-&gt;assertArrayHasKey('id', $item);
            $this-&gt;assertArrayHasKey('name', $item);
            $this-&gt;assertArrayHasKey('note', $item);
        }
    }</code></pre>
<p>执行单元测试的效果：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Tests$ phpunit --filter testGetMultiBaseInfo ./Api/Api_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

.

Time: 23 ms, Memory: 6.25Mb

OK (1 test, 13 assertions)</code></pre>
<p>搞定，收工，开饭！</p>
<hr />
<h4><a href="/wikis/%5B2.12%5D-%E6%B5%B7%E9%87%8F%E6%95%B0%E6%8D%AE%EF%BC%9A%E5%8F%AF%E9%85%8D%E7%BD%AE%E7%9A%84%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.14%5D-%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%EF%BC%9A%E6%84%8F%E5%9B%BE%E5%AF%BC%E5%90%91%E7%BC%96%E7%A8%8B%E4%B8%8B%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91.html">下一章</a>#[2.14]-测试驱动开发：意图导向编程下的接口开发</h4>
<h4><a href="/wikis/%5B2.13%5D-%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95%EF%BC%9A%E5%9C%A8%E7%BA%BFSQL%E8%AF%AD%E5%8F%A5%E6%9F%A5%E7%9C%8B%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.15%5D-%E6%BC%94%E8%BF%9B%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%E7%BB%AD%E7%AF%87.html">下一章</a></h4>
<hr />
<p><em>有的时候宁愿付钱让你一周在床上待着，也不想让你用这周剩下的时间去调试你在周一所写的代码。 --丹·所罗门</em>  </p>
<h2>2.14.1 测试驱动开发</h2>
<p>做正确的事，比把事情做正确更为重要。  </p>
<p>当明确需要做何事后，再通过事先编写单元测试来准确表达我们将要实现的功能，是相当具有指导意义的。你会发现接下来你的开发历程就是：单元测试-设计-重构，而且这种正向循环是很有创造性的，并且进行到一定程度后会慢慢体会到浮现式设计的乐趣。  </p>
<p>关于测试驱动开发TDD，有很多资料已进行了说明，这里不再赘述。  </p>
<p>如果还没了解PHPUnit，可先阅读：<a href="https://phpunit.de/manual/3.7/zh_cn/automating-tests.html">PHPUnit 手册</a><br />
如果还没了解PHPUnit，可先阅读：<a href="https://phpunit.de/manual/3.7/zh_cn/automating-tests.html">PHPUnit 手册</a><br />
如果还没了解PHPUnit，可先阅读：<a href="https://phpunit.de/manual/3.7/zh_cn/automating-tests.html">PHPUnit 手册</a>  </p>
<h2>2.14.2 意图导向编程</h2>
<p><img src="http://webtools.qiniudn.com/20150411005257_2bc8683a50ac1d620cef4e39918ed3e5" alt="apic" />  </p>
<p>在编写代码前，先写测试代码，更容易提高 <strong>关注点</strong> 。  </p>
<p>因为，在开发过程中， 大多时候会被外界打断（如需求沟通、线上问题处理、临时会议等），而通过单元测试则可以让你“几乎忘却需要做什么”的情况下重新让你回到之前的状态，特别在并行开发多个不同项目的需求时尤其重要。  </p>
<p>除此之外，遵循“红-绿-重构”这样的流程，我们可以在更高的层面关注需要实现的功能需求，并自顶而下地进行设计优化，精益代码。  </p>
<h2>2.14.3 编写测试的原则、模式和指导</h2>
<p>首先应该意识到，测试代码和生产代码一样重要。其次，测试代码也应该和生产代码一样被同步维护更新，这样才能保持生气，更大地发挥作用。只有当不断地对测试的代码进行修修补被，我们才能保持自动化测试这张“安全网”常新。</p>
<h2>2.14.4 F.I.R.S.T.原则</h2>
<ul>
<li>快速 Fast</li>
<li>独立 Independent</li>
<li>可重复 Repeatable</li>
<li>自足验证 Self-validating</li>
<li>及时 Timely</li>
</ul>
<h2>2.14.5 构造-操作-检验（BUILD-OPERATE-CHECK）模式</h2>
<p>这个模式也可以理解成：“当... 做...应该...”。其中，构造包括测试环境的搭建、测试数据前期的准备；操作是指对被测试对象的调用， 以及被测试对象之间的通信和协助交互；最后检验则是对业务规则的断言、对功能需求的验证。</p>
<h2>2.14.6 如何编写高效测试代码</h2>
<ul>
<li>1、与产品代码分开，与测试代码对齐</li>
<li>2、利用测试骨架（phpunit-skelgen或者自定义生成器）自动生成测试代码</li>
<li>3、使用测试替身、测试桩构建昂贵资源、制造异常情况</li>
<li>4、每个测试一个概念</li>
</ul>
<h2>2.14.7 PhalApi开发下的单元测试</h2>
<p>我们推荐在各自的项目代码中平行编写单元测试，并逐渐完善、保持同步。以下是进行单元测试的参考。</p>
<h3>(1)Api接口层的单元测试</h3>
<p>Api接口层，是我们后台开发的主要切入点，也是直接对外提供服务的入口，属于更高层次的概念并拥有指定的业务功能，更是后台开发的关注点。所以在对新接口进行开发前，编写单元测试是非常有意义的。  </p>
<p>为了可以自动生成测试代码，我们可以先简单定义好接口的函数签名（以获取用户基本信息接口为例）：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Api/User.php 
&lt;?php

class Api_User extends PhalApi_Api {

    public function getBaseInfo() {
    }

}</code></pre>
<p>随后，自动生成测试代码骨架：</p>
<pre><code class="language-javascript">$ mkdir ./Demo/Tests/Api -p
$ cd ./Demo/Tests/Api
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Api/User.php Api_User ./Public/init.php &gt; ./Demo/Tests/Api/Api_User_Test.php</code></pre>
<p>根据接口的需要，验证接口返回的格式，以及业务数据的正确性。</p>
<pre><code class="language-javascript">//$ vim ./Demo/Tests/Api/Api_User_Test.php
    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&amp;user_id=1';

        //Step 2. 执行请求  
        $rs = PhalApi_Helper_TestRunner::go($url);

        //Step 3. 验证
        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('msg', $rs);
        $this-&gt;assertArrayHasKey('info', $rs);

        $this-&gt;assertEquals(0, $rs['code']);

        $this-&gt;assertEquals('dogstar', $rs['info']['name']);
        $this-&gt;assertEquals('oschina', $rs['info']['note']);
    }</code></pre>
<p>上面的验证意思简单明了，结合 <strong>构造-操作-检验（BUILD-OPERATE-CHECK）模式</strong> 加以说明一下。  </p>
<h3>构造：构建请求URL</h3>
<pre><code class="language-javascript">        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&amp;user_id=1';</code></pre>
<p>此参数即对应接口请求的URL参数，我们将此参数追加在接口入口并在浏览器打开可以得到同样的接口执行效果。但这样的好处更在于通过单元测试帮我们记住了各种接口测试的业务场景。而不再是像以前那样打开N个浏览器窗口人工进行调试，也不用像以前那样苦苦寻找浏览器记录。</p>
<p>如果接口需要POST数据，或者其他更多参数，可以使用$params来传递更多参数，一如：  </p>
<pre><code>        //Step 1. 构建请求URL
        $url = 'service=User.GetBaseInfo&amp;user_id=1';
        $params = array(); //更多参数

        //Step 2. 执行请求  
        $rs = PhalApi_Helper_TestRunner::go($url, $params); //通过第二个参数，传送更多参数</code></pre>
<h4>操作：执行请求</h4>
<p>这里的操作，显然就是对应我们接口的调用。简单地如：</p>
<pre><code class="language-javascript">        //Step 2. 执行请求  
        $rs = PhalApi_Helper_TestRunner::go($url);</code></pre>
<p>这样，便可以在服务端模拟进行一次接口的请求调度，注意这里是在服务端进行的接口请求，而不是客户端。<br />
此外，如果需要传递更多参数，可以参考前面的示例。这里简单补充一下PhalApi_Helper_TestRunner测试辅助类的接口签名说明：  </p>
<pre><code>&lt;?php
class PhalApi_Helper_TestRunner {

    /**
     * @param string $url 请求的链接
     * @param array $param 额外POST的数据
     * @return array 接口的返回结果
     */
    public static function go($url, $params = array()) {

        ... ...</code></pre>
<h4>检验：验证</h4>
<p>在对接口返回的结果中，我们可以这样依次进行正确性的验证：</p>
<ul>
<li>1、先验证接口返回的格式是否正确，有无字段遗漏；</li>
<li>2、返回的业务数据是否正确；</li>
</ul>
<pre><code class="language-javascript">        //Step 3. 验证
        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('msg', $rs);
        $this-&gt;assertArrayHasKey('info', $rs);

        $this-&gt;assertEquals(0, $rs['code']);

        $this-&gt;assertEquals('dogstar', $rs['info']['name']);
        $this-&gt;assertEquals('oschina', $rs['info']['note']);</code></pre>
<p>由于测试环境的数据变动频繁，所以我们可以针对个别的接口进行更精确的验证，而对类似列表获取这样的大批量的数据，则校验其结构格式。<br />
除此之外，还有一种情况也是需要纳入检验，即除了上面的正常请求情况下的 <strong>异常请求</strong> 。  </p>
<p>接下来的即是之前文档里面所说的单元测试执行和接口开发，此处略。  </p>
<h3>(2)Domain层和Model层的单元测试</h3>
<p>下面继续简单补充一下之前没谈及到的Domain层和Model层的单元测试。   </p>
<p>显然，这两层的开发，已经在前面的接口测试驱动开发的指导下很好地完成了。现在可以快速追加对这两层的单元测试。得益于我们的生成测试骨架的脚本，操作如下：</p>
<pre><code class="language-javascript">$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Domain/User.php Domain_User &gt; ./Demo/Tests/Domain/Domain_User_Test.php
$ php ./PhalApi/build_phpunit_test_tpl.php ./Demo/Model/User.php Model_User &gt; ./Demo/Tests/Model/Model_User_Test.php</code></pre>
<p>接着，修改一下测试环境 <strong>test_env.php</strong>的引用路径：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Tests/Domain/Domain_User_Test.php 
//$ vim ./Demo/Tests/Model/Model_User_Test.php

require_once dirname(__FILE__) . '/../test_env.php';</code></pre>
<p>各自完善一下单元测试：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Tests/Domain/Domain_User_Test.php

    /**
     * @group testGetBaseInfo
     */
    public function testGetBaseInfo()
    {
        $userId = '1';

        $rs = $this-&gt;domainUser-&gt;getBaseInfo($userId);

        $this-&gt;assertArrayHasKey('id', $rs);
        $this-&gt;assertArrayHasKey('name', $rs);
        $this-&gt;assertArrayHasKey('note', $rs);

        $this-&gt;assertEquals('dogstar', $rs['name']);
    }</code></pre>
<p>执行一下：</p>
<pre><code class="language-javascript">$ phpunit ./Demo/Tests/Domain/Domain_User_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

.

Time: 49 ms, Memory: 6.25Mb

OK (1 test, 4 assertions)</code></pre>
<h3>(3)Model层的单元测试</h3>
<p>Model层的单元测试类似，不再赘述。</p>
<h2>2.14.7 更进一步的单元测试套件</h2>
<p>到目前为止，我们有了如下的产品代码：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Demo$ tree
.
├── Api
│   └── User.php
├── Domain
│   └── User.php
├── Model
│   └── User.php</code></pre>
<p>并拥有了与之平行对应的单元测试：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Tests$ tree
.
├── Api
│   └── Api_User_Test.php
├── Domain
│   └── Domain_User_Test.php
├── Model
│   └── Model_User_Test.php
└── test_env.php</code></pre>
<p>这样是一个很好的开始，但若我们每次测试都分别调用三次这些不同层次的单元测试，显然有点不科学。所以，利用PHPUnit的配置文件，我们可以轻松管理我们的测试套件，如：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Tests$ vim ./phpunit_user_getbaseinfo.xml 

&lt;?xml version="1.0" encoding="UTF-8"?&gt;

&lt;phpunit backupGlobals="false"
    ...

    &lt;testsuites&gt;
        &lt;testsuite name="Test Suite"&gt;
            &lt;file&gt;./Api/Api_User_Test.php&lt;/file&gt;
            &lt;file&gt;./Domain/Domain_User_Test.php&lt;/file&gt;
            &lt;file&gt;./Model/Model_User_Test.php&lt;/file&gt;
        &lt;/testsuite&gt;
    &lt;/testsuites&gt;
&lt;/phpunit&gt;</code></pre>
<p>啊哈！终于，当需要调用这些分布在不同目录位置的单元测试时，只需要这么简单的一行命令：</p>
<pre><code class="language-javascript">dogstar@ubuntu:Tests$ phpunit -c ./phpunit_user_getbaseinfo.xml 
PHPUnit 4.3.4 by Sebastian Bergmann.

.....

Time: 54 ms, Memory: 7.25Mb

OK (5 tests, 28 assertions)</code></pre>
<h2>2.14.8 这样的好处？</h2>
<p>上面的过程，细节较多，而且需要实际操作的部分也比较多。对于之前没有接触过单元测试这块的同学，可能会有点迷茫，对于不愿意接受单元测试的同学来说更加枯燥。  </p>
<p>然而，然而。  当我们把越痛苦的事情越早完成后，我们后面就顺畅多了。正如在某一次培训中的某一位敏捷开发的专家所说的： <em>要逐步对小问题做优化，而不是要等到大问题到来时再做变革</em> 。  </p>
<h3>那这样的好处在于哪里呢？</h3>
<p>这里不就理论回答，而是以我个人的经历来简单说明。  </p>
<p>首先，正如上面所说的，单元测试帮你很好地记住并整理了各种接口测试的场景，而不用再像以前那样打开N个浏览器窗口逐个人工校对。<br />
其次，在单元测试的论证下我们可以更有信心地跟测试说、跟产品说、跟发布说我们的代码没问题，因为我们通过严格的单元测试，而不是人为主观上的想当然应该不会有问题吧。<br />
最后，也是最重要的，在后期的接口升级、改动和维护中，单元测试再一次为我们提供了保护，犹如一张安全网，涵盖我们改动的每一处代码。与此同时，对于重构也亦然。 </p>
<p>但单元测试所带给你的，不仅仅是上面所说的简单这几点。更多地完全不一样的开发历程，而其中滋味和令人兴奋的体现，只有当你亲自去尝试才会明白其中滋味。So, try it by yourself.</p>
<hr />
<h4><a href="/wikis/%5B2.13%5D-%E6%8E%A5%E5%8F%A3%E8%B0%83%E8%AF%95%EF%BC%9A%E5%9C%A8%E7%BA%BFSQL%E8%AF%AD%E5%8F%A5%E6%9F%A5%E7%9C%8B%E4%B8%8E%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.15%5D-%E6%BC%94%E8%BF%9B%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%E7%BB%AD%E7%AF%87.html">下一章</a>#[2.15]-演进：新型计划任务续篇</h4>
<h4><a href="/wikis/%5B2.14%5D-%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%EF%BC%9A%E6%84%8F%E5%9B%BE%E5%AF%BC%E5%90%91%E7%BC%96%E7%A8%8B%E4%B8%8B%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.16%5D-%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%BA%94%E5%AF%B9%E5%A4%8D%E6%9D%82%E9%A2%86%E5%9F%9F%E4%B8%9A%E5%8A%A1%E7%9A%84Domain%E5%B1%82-.html">下一章</a></h4>
<hr />
<p><em>计算机科学中的任何问题都可以用另外的间接层解决，但是这通常会引发另一个问题。  -- David Wheeler</em>  </p>
<h2>2.15.1 新型计划任务回顾</h2>
<p>在 <a href="/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">[1.31]-新型计划任务：以接口形式实现的计划任务</a> 一章中，我们讨论了PhalApi中对计划任务的设计和底层实现。 </p>
<p>但对于很多应用，很多项目，或者很多同学来说，仍然比较广泛，不能直接使用。<br />
这一章则专门为此而进行演进，并提供最终可用的计划任务调度，同时我们也会阐明如何进行扩展定制。</p>
<p>也就是说，这一章将提供Task扩展类库的统一调度方式，以便在启动crontab任务后，可以通过数据库简单配置，即可执行各种任务。  </p>
<h2>2.15.2 最终调度的方式：crontab</h2>
<p>出于对业务的考虑，我们首先需要明确此crontab调度方式所支持的功能，它应该包括但不限于：  </p>
<ul>
<li>1、通过简单的数据库配置，即可启动一个新的任务</li>
<li>2、具备循环调度的能力，并能初步防止并发调度</li>
<li>3、可以对异常的任务进行修复</li>
<li>4、优先执行太远未执行的任务</li>
<li>5、支持本地和远程两种调度方式、三种MQ类型，以及扩展的能力</li>
</ul>
<h2>2.15.3 核心时序图与分层</h2>
<p>在原来的时序图基础上，我们可以进行演进的设计，追加了统一的调度后如下所示：<br />
<img src="http://webtools.qiniudn.com/task_6.jpg" alt="a pic" />   </p>
<p>通过上面详细的时序图，我们可以发现里面的设计是出于这样的分层考虑：  </p>
<table>
<thead>
<tr>
<th>序号</th>
<th>层</th>
<th>关键操作</th>
<th>说明</th>
<th>如何使用</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>启动脚本</td>
<td>crontab.php</td>
<td>操作crontab执行的脚本</td>
<td>客户端可以进行必要的初始化工作</td>
</tr>
<tr>
<td>2</td>
<td>进程级</td>
<td>Task_Progress::run()</td>
<td>根据进程配置的数据库表，进行循环调度</td>
<td>不需要改动，直接使用</td>
</tr>
<tr>
<td>3</td>
<td>触发器</td>
<td>Task_Trigger::fire()</td>
<td>进行计划任务调度的上下文环境，用于指定runner和mq类型</td>
<td>客户端也可进行定制扩展，进行必要的操作</td>
</tr>
<tr>
<td>4</td>
<td>MQ消费与调度</td>
<td>Task_MQ::pop()和Task_Runner::go()</td>
<td>不断消费MQ队列，并依次进行调度</td>
<td>不需要改动，直接使用，也可扩展</td>
</tr>
<tr>
<td>5</td>
<td>计划任务服务</td>
<td>PhalApi_Api::doSth()</td>
<td>执行计划任务服务</td>
<td>由客户端按接口形式实现</td>
</tr>
</tbody>
</table>
<p>虽然上面的层级，初看起来有点多，但我们再次验证了计算机那个伟大的定论：计算机的任何问题都可以通过一个中间层来解决。  </p>
<p>由此看出，上面的层级其实相当于：</p>
<pre><code>客户端初始化 --&gt; 直接使用 --&gt; 自由组合与操作 --&gt; 直接使用 --&gt; 任务服务实现</code></pre>
<h2>2.16.4 进程配置的数据库表设计</h2>
<pre><code class="language-javascript">CREATE TABLE `phalapi_task_progress` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(200) DEFAULT '' COMMENT '任务标题',
  `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
  `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
  `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
  `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
  `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
  `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
  `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;</code></pre>
<p>对此表的关键字段说明如下：</p>
<table>
<thead>
<tr>
<th>字段</th>
<th>说明</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td>trigger_class</td>
<td>触发器的类名</td>
<td>须实现Task_Progress_Trigger::fire($params)接口</td>
</tr>
<tr>
<td>fire_params</td>
<td>触发器的参数</td>
<td>加传给Task_Progress_Trigger::fire()函数的参数，格式为：service&amp;MQ类名&amp;runner类名</td>
</tr>
<tr>
<td>interval_time</td>
<td>执行间隔</td>
<td>单位为秒</td>
</tr>
<tr>
<td>enable</td>
<td>是否启动</td>
<td>此字段禁止时，将不再执行</td>
</tr>
<tr>
<td>state</td>
<td>进程状态</td>
<td>当此状态一直为异常或者运行且超过1天时，系统会进行修复，即重置为空闲状态</td>
</tr>
</tbody>
</table>
<p>其中，对于fire_params参数，MQ类名和runner类名可选，以下是一些示例：</p>
<pre><code class="language-javascript">//示例1：完整的配置
//fire_params=Task_Demo.DoSth&amp;Task_MQ_DB&amp;Task_Runner_Local
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);
$runner-&gt;go('Task_Demo.DoSth');

//示例2：使用默认的Runner
//fire_params=Task_Demo.DoSth&amp;Task_MQ_DB
$mq = new Task_MQ_DB();
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner-&gt;go('Task_Demo.DoSth');

//示例3:使用默认的MQ和默认的Runner
//fire_params=Task_Demo.DoSth
$mq = new Task_MQ_Redis(); //默认使用redis的MQ
$runner = new Task_Runner_Local($mq);  //默认使用本地Runner
$runner-&gt;go('Task_Demo.DoSth');

//示例4：使用自定义的MQ和Runner
//fire_params=Task_Demo.DoSth&amp;My_MQ&amp;My_Runner
class My_MQ implements Task_MQ {
     // ...
}
class My_Runner extends Task_Runner {
     // ...
}
$mq = new My_MQ();
$runner = new My_Runner($mq);
$runner-&gt;go('Task_Demo.DoSth');</code></pre>
<h2>2.15.5 运行效果</h2>
<p>最终的效果就是，我们通过这样两行简单的代码，即可实现一系列复杂的任务调度：</p>
<pre><code class="language-javascript">$progress = new Task_Progress();
$progress-&gt;run();</code></pre>
<p>让我们来看下这样设计的运行效果吧！看下这两行代码背后所产生的魔力。  </p>
<p>首先，我们先添加两条计划任务：</p>
<pre><code class="language-javascript">INSERT INTO `phalapi_task_progress` VALUES ('1', 'test demo', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&amp;Task_MQ_File&amp;Task_Runner_Local', '300', '1', '', '0', '0');
INSERT INTO `phalapi_task_progress` VALUES ('2', 'test ok', 'Task_Progress_Trigger_Common', 'Default.Index&amp;Task_MQ_DB&amp;Task_Runner_Local', '100', '1', '', '0', '0');</code></pre>
<p>然后，伪造一些MQ：</p>
<pre><code class="language-javascript">INSERT INTO `phalapi_task_mq_0` VALUES ('8', 'Default.Index', '', '0', '');</code></pre>
<p>最后，生成单元测试：</p>
<pre><code class="language-javascript">&lt;?php
class PhpUnderControl_TaskProgress_Test extends PHPUnit_Framework_TestCase
{
    public $taskProgress;

    protected function setUp()
    {
        parent::setUp();

        $this-&gt;taskProgress = new Task_Progress();
    }

    /**
     * @group testRun
     */
    public function testRun()
    {
        $rs = $this-&gt;taskProgress-&gt;run();
    }

}
</code></pre>
<p>并执行之：</p>
<pre><code class="language-javascript">$ phpunit ./Task_Progress_Test.php

[1 - 0.06666s]SELECT id, title FROM phalapi_task_progress WHERE (state != ?) AND (last_fire_time &lt; ?) AND (enable = ?) ORDER BY last_fire_time ASC; -- 0, 1431965153, 1&lt;br&gt;
[2 - 0.07002s]SELECT id, title, trigger_class, fire_params FROM phalapi_task_progress WHERE (state = 0) AND (interval_time + last_fire_time &lt; ?) AND (enable = ?); -- 1432051553, 1&lt;br&gt;
[3 - 0.06549s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '1');&lt;br&gt;
[4 - 0.07432s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '1');&lt;br&gt;
[5 - 0.06469s]UPDATE phalapi_task_progress SET result = '{\"total\":0,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '1');&lt;br&gt;
[6 - 0.06746s]SELECT enable, state FROM phalapi_task_progress WHERE (id = '2');&lt;br&gt;
[7 - 0.07043s]UPDATE phalapi_task_progress SET state = 1 WHERE (id = '2');&lt;br&gt;
[8 - 0.06673s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;&lt;br&gt;
[9 - 0.48185s]DELETE FROM phalapi_task_mq_0 WHERE (id IN ('8'));&lt;br&gt;
[10 - 0.06514s]SELECT id, params FROM phalapi_task_mq_0 WHERE (service = 'Default.Index') ORDER BY id ASC LIMIT 0,10;&lt;br&gt;
[11 - 0.50694s]UPDATE phalapi_task_progress SET result = '{\"total\":1,\"fail\":0}', state = 0, last_fire_time = 1432051553 WHERE (id = '2');&lt;br&gt;

Time: 1.98 seconds, Memory: 6.50Mb

OK (1 test, 0 assertions)</code></pre>
<p>查看对比一下数据库，目前发现运行良好！  </p>
<p>提交代码，保存文档，收工睡觉！</p>
<h2>2.15.6 演进的乐趣</h2>
<p>得益于前期良好的设计以及底层支持，我们发现，在提供这样一种统一的调度方式是非常方便的。  </p>
<p>不仅如此，如果你明白了其中的设计，需要进行定制和扩展也是非常方便的。也就是说，我们不仅提供了一种具体实际可用的方式，也提供了广阔自由的扩展空间。具体与抽象，两者仍可得。  </p>
<p>然而，这一切不仅依赖于良好的设计，还依赖于测试驱动开发下的浮现式设计。</p>
<hr />
<h4><a href="/wikis/%5B2.14%5D-%E6%B5%8B%E8%AF%95%E9%A9%B1%E5%8A%A8%E5%BC%80%E5%8F%91%EF%BC%9A%E6%84%8F%E5%9B%BE%E5%AF%BC%E5%90%91%E7%BC%96%E7%A8%8B%E4%B8%8B%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.16%5D-%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%BA%94%E5%AF%B9%E5%A4%8D%E6%9D%82%E9%A2%86%E5%9F%9F%E4%B8%9A%E5%8A%A1%E7%9A%84Domain%E5%B1%82-.html">下一章</a>#[2.16]-领域驱动设计：应对复杂领域业务的Domain层</h4>
<h4><a href="/wikis/%5B2.15%5D-%E6%BC%94%E8%BF%9B%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%E7%BB%AD%E7%AF%87.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.17%5D-%E5%BE%AE%E6%9C%8D%E5%8A%A1%EF%BC%9AApi%E6%8E%A5%E5%8F%A3%E6%9C%8D%E5%8A%A1%E5%B1%82.html">下一章</a></h4>
<hr />
<p><em>温馨提示：此篇章需要比较长的时间才能最终定稿，因为我还要寻找最合适的方式和语言来表述。</em></p>
<h2>2.16.1 领域驱动设计</h2>
<p>很多框架关心性能，而不关心人文；很多项目关心技术，而不关注业务。  </p>
<p>就这造成了复杂的领域业务在项目中得不到很好地体现和描述，也没有统一的规则，更没有释意的接口。最终导致了在“纯面向对象”框架里面凌乱的代码编写，为后期的维护扩展、升级优化带来很大的阻碍。这就变成了，框架只关注性能，项目只关心技术，而项目却可怜地失去了演进的权利，慢慢地步履维艰，最终牵一发而动全身。  </p>
<p><strong>很多人都不知道该如何真正应对和处理领域的业务</strong> ，尽管领域业务和单元测试都是如此重要并被广泛推崇。正如同表面上我们都知道单元测试却没有具体真实地接触过，并且一旦到真正需要编写一行单元测试的代码时就傻眼了。  </p>
<p>这里不是发明一些新技术，也不是提供一些新的模式，而是继续将前人、大神和顶级大师关于领域驱动设计这方面的思想结合真实后台接口开发进行分享，进而推广之。</p>
<h2>2.16.2 讲述故事</h2>
<p>很多人，都喜欢听故事。像我以前中学的时候，就很喜欢看《故事会》。  </p>
<p>如果，我们能让代码也像小说一样，在讲述某个故事时，将会更加吸引“读者”（也就是其他开发同学），从而易于理解和维护。   </p>
<p>最近，我在做一个项目时，再一次发现了这种讲述故事的威力。  </p>
<h3>(1)一个第三方登录的写法</h3>
<p>我们先以F项目来命名这个项目，在F项目中，我们跟其他App一样，需要接入第三方登录，其中包括：微信登录、微博登录和QQ登录、邮箱登录等。  </p>
<p>以下，则是我根据 <strong>讲述故事</strong>  的方式，为微信登录编写的代码：</p>
<pre><code class="language-javascript">&lt;?php

class Api_User_Login extends PhalApi_Api {

    public function getRules() {
        return array(
            'weixin' =&gt; array(
                'openId' =&gt; array('name' =&gt; 'wx_openid', 'require' =&gt; true, 'min' =&gt; 1, 'max' =&gt; 28),
                'token' =&gt; array('name' =&gt; 'wx_token', 'require' =&gt; true, 'min' =&gt; 1, 'max' =&gt; 150),
                'expiresIn' =&gt; array('name' =&gt; 'wx_expires_in', 'require' =&gt; true, 'min' =&gt; 1),
                'nickname' =&gt; array('name' =&gt; 'name', 'default' =&gt; '',),
                'avatar' =&gt; array('name' =&gt; 'avatar', 'default' =&gt; '',),
            ),
        );
    }

    public function weixin()
    {
        $rs = array('code' =&gt; 0, 'info' =&gt; array(), 'msg' =&gt; '');

        $domain = new Domain_User_Login_Weixin();
        $isFirstBind = $domain-&gt;isFirstBind($this-&gt;openId);

        $userId = 0;
        if ($isFirstBind) {
            $userId = Domain_User_Generator::createUserForWeixin(
                   $this-&gt;openId, $this-&gt;nickname, $this-&gt;avatar);

            $domain-&gt;bindUser($userId, $this-&gt;openId, $this-&gt;token, $this-&gt;expiresIn);
        } else {
            $userId = $domain-&gt;getUserIdByWxOpenId($this-&gt;openId);
        }

        $token = Domain_User_Session::generate($userId, $this-&gt;client);

        $rs['info']['user_id'] = $userId;
        $rs['info']['token'] = $token;
        $rs['info']['is_new'] = $isFirstBind ? 1 : 0;

        return $rs;
    }
}</code></pre>
<blockquote>
<p>温馨提示：<br />
以下代码为我正在参与开发的一个项目的源代码，已征得项目负责人同意。同时出于对项目的尊重，已省去部分代码。  </p>
</blockquote>
<h3>(2)登录场景的故事</h3>
<p>细细品读上面的代码，其实就是在描述登录场景的故事：<br />
当用户进行微信登录时，先查看用户是否首次登录；如果是，则为用户自动生成了一个帐号并绑定，如果不是，则获取已绑定的用户ID；最后，生成一个登录态的token。  </p>
<p>当然，这里为了突出故事的主线，已去除了很多异常情况的处理。</p>
<h3>(3)有趣的开发体验</h3>
<p>更为有趣的是，此次参与F项目开发的还有另外一位同学。<br />
这位同学拥有多年资深的iOS开发经验，但对PHP开发还是首次接触，但他在参考微信登录的写法后，很快就交付了微博和QQ登录这两个接口服务。  </p>
<p>但令我为之惊讶和兴奋的不是他的速度，而是他所编写的代码是如此的优雅美丽，犹如出自资深PHP开发人员之手。  </p>
<p>这让我再一次相信，使用 <strong>讲述故事</strong> 的方式来开发接口，不仅能让代码更易于传送业务逻辑，也能为更多的同学乃至新手接受并快速上手。</p>
<h3>(4)与TDD的结合</h3>
<p><strong>讲述故事</strong> 有一个很明显的特点就是，全部的操作都是处于同一抽象级别的，即都是释意接口下的领域业务规则和操作。  </p>
<p>但对于如何引出这个业务场景，很多人用传统的方式都是写一个接口，然后在浏览器调试。  </p>
<p>其实，这并不是最好的开发体验。因为，使用这种传统的开发方式，你难免会落入技术缠绕的纠结中，比如在想使用哪些类型的数据库表字段。也就是说，你在丢失关注点。  </p>
<p>而通过测试驱动，则会先引导你做正确的事，再将你的关注引导到领域业务上，最后将自然而然地就知道应用使用什么技术了。</p>
<p>讲故事，是针Domain领域层外部使用的说明。下面，我们将走进Domain层内部，阐明我们应该如何为讲故事做好准备。</p>
<h2>2.16.3 表达规则</h2>
<h3>(1)释意接口</h3>
<p>释意接口的作用是很大的，这可以使得后来的同学在看待一个接口时，无须深入内部实现即可明白它的用意和产生的影响。<br />
如一个get系列的操作，我们可以推断出它是无副作用的。但如果当时的开发者不遵守约定，在里面作了一些“手脚”，则会破坏我们这些“望文生义”的推断。    </p>
<p>在我曾经就职的一个游戏公司里面，我常根据接口的命名来推断它的作用，但往往会倍受伤害。因为以前的开发人员没有遵守这些约定，当时的team leader还责怪我不能太相信这些接口的命名。然而我想，如果我们都不能相信我们团队其他人员的接口，我们又能相信谁呢？我们是否应该反思，是否应该遵守约定编程所带来的好处？<br />
任何一个问题，都不是个人的问题，而是一个团队的问题。如果我们经常不断地发生一生项目的问题而要去指责某个人时，我们又为何不从一开始就遵守约定而去避免呢？  </p>
<p>简单来说，释意接口会将“命令-查询”分离、会将多个操作分解成更小粒度的操作而保持同一层面的处理。根据《领域驱动设计》一书的说法：<br />
<em>类型名、方法名和参数名一起构成了一个释意接口（Intention-Revealing Interface），以解释设计意图，避免开发人员需要考虑内部如何实现，或者猜测。</em>  </p>
<p>如下面的家庭组成员领域业务类：</p>
<pre><code class="language-javascript">&lt;?php
class Domain_Group_Member {

    public function joinGroup($userId, $groupId) {
        //TODO
    }

    public function hasJoined($userId, $groupId) {
        //TODO
    }

}</code></pre>
<p>我们可以知道，Domain_Group_Member::joinGroup()用于加入家庭组，会产生副作用，是一个命令操作；Domain_Group_Member::hasJoined()用于检测用户是否已加入家庭组，无副作用，则是一个查询操作。</p>
<h3>(2)业务规则的描述</h3>
<p><em>规则出现且仅出现一次。</em>  </p>
<p>当代码出现重复时，我们都知道会面临维护的高成本。而当规则多次出现时，我们更知道当规则发生变化时所带来的各种严重的问题，这也正是为什么总有一些这样那样的BUG的原因。<br />
系统出现问题，大多数上都是业务的问题。而业务的问题在于我们不能把规则收敛起来，汇集于一处。  </p>
<p>在以往的开发中，我都很注意对这些规则统一的重构工作。这使得我可以非常相信我所提供业务的稳定性，以及在给别人解讲时的信心。<br />
如有一次，我们有一个大型的系统中的一个页面跳转链接的生成规则，后来系统进行了调整，需要对URL生成规则作出调整。我跟另一位新来的同事说只需改一处时，他仍然很惊讶地问我怎么可能？！因为他看到是这么多场景，如此多的页面，怕会有所遗漏。然而，事实证明，我们确实只需要改动一处就可以了。  </p>
<p>类似这样的URL拼接规则，我们可以这样表示：</p>
<pre><code class="language-javascript">&lt;?php
class Domain_Page_Helper {

    public static function createUrl($userId) {
        return DI()-&gt;config-&gt;get('app.web.host') . '/u/' . $userId;
    }
}</code></pre>
<p>正如你看到了，我们使用了static静态方法，是因为这个规则生成可以当作一个工具方法来使用。我们不反对使用static方法，但推荐只在合适的时候使用。  </p>
<p>规则出现且仅出现一次，可以说是一个知易行难的做法，因为我们总会有不经意间重复实现规则。有时我们会忽略已有的规则，有时我们会出于当前紧张开发进度的考虑，有时我们可能懒得去统一。<br />
但把规则的实现统一起来，再重复调用，会让你在今后的项目开发中，长期收益。没错，真的会长期收益。  </p>
<h2>2.16.4 不可变值与无状态操作</h2>
<h3>(1)在开源中国上翻译的两点收获</h3>
<p>首先，让我们简单来了解一下PHP语言的运行机制。<br />
PHP是一个运行于服务端的脚本解析语言，每一个HTTP请求都会触发一个php-fpm进程来响应，所以不同于其他长时间运行的语言或者系统，不用过多地考虑内存的回收或者对实体的管理和共享。  </p>
<p>这样是有明显的好处，作为PHP开发人员，由于每一次请求所消耗的内存都会在本次释放，即使运行错误也不会影响其他的调用。从而，我们可以放心快速地开发。<br />
但我们也应该看到这样的便利给很多开发同学所带来的误导。正因为不用再担心一些传统的问题（如内存管理），他们变得更无限制。当这种无限制日积月累而引发诸多项目的问题时，他们会开始责怪PHP这门语言。  </p>
<p>其实，语言本身没有对错，关键在于我们怎么使用。  </p>
<p>先前，在开源中国进行翻译时，我从翻译的文章中收获了两点。  </p>
<ul>
<li>第一点是，学习不同的语言，你将会获得不同的灵感。比如，你是OC的开发人员，可以从GO语言中获得OC的开发灵感；反之亦然。  </li>
<li>第二点是，不可变值的使用。  </li>
</ul>
<p>这里，我将尝试说明如何在PhalApi现有的分层机制基础上，结合不可变值和无状态，应对复杂的领域业务开发。  </p>
<h3>(2)不可变值</h3>
<p>通常，我们在程序中处理的变量可以分为：值和实体。简单来说，值是一些基本的类型，如整数、布尔值、字符串；实体则是类对象，有自己内部的状态。当一个实体表示一个值的概念时（如坐标、金额、日期等），我们可以称之为值对象。<br />
明显地，系统的复杂性不在于对值的处理，而在于对一系列实体以及与其关联的另一系列实体间的处理。  </p>
<p>如同其他语言一样，如果我们也在PHP遵循 <strong>不可变值</strong> 与 <strong>无状态</strong> 这两个用法，我们的系统乃至业务都可以从中获益。  </p>
<p><strong>不可变值</strong> 是指一个实体在创建后，其内部的状态是不可变更的，这样就能在系统内放心地流通使用，而无须担心有副作用。  </p>
<p>举个简单的例子，在我们国际交易系统中有一个金额为100RMB的对象，表示用户此次转账的金额。如果此对象是不可变值，那么我们在系统内，无论是计算手费、日记纪录，还是转账事务或其他，我们都能信任此对象放心使用，不用担心哪里作了篡改而导致一个隐藏的致使BUG。  </p>
<p>也就说，当你需要修改此类对象时，你需要复制一个再改之。有人会担心new所带来的内存消耗，但实际上，new一个只有一些属性的对象消耗很少很少。  </p>
<p>要明白为什么在修改前需要再创建新的对象，也是很容易理解的。首先，我们保持了和基本类型一致的处理方式；其次，我们保持了概念的一致性，如坐标A（1，2）和坐标B（1，3）是两个不同的坐标。<br />
当坐标A发生改变，坐标A就不再是原来的坐标A，而是一个新的坐标。从哲学角度上看，这是两个不同的概念。  </p>
<p>在PhalApi中，我们可以看到不可变值在Query对象中的应用：  </p>
<pre><code class="language-javascript">$query1 = new PhalApi_ModelQuery();
$query1-&gt;id = 1;

$query2 = new PhalApi_ModelQuery($query1-&gt;toArray());
$query2-&gt;id = 2;</code></pre>
<p>这样以后，我们就不再需要小心翼翼维护“漂洋过海”的值对象了，而是可以轻松地逐层传递，这有点像网络协议的逐层组装。  </p>
<p>这又让我想起了《领域驱动设计》一书中较为中肯的说法：<br />
<em>把值对象看成是不可变的。不要给它任何标识，这样可以避免实体的维护工作，降低设计的复杂性。</em>  </p>
<h3>(3)无状态操作</h3>
<p>前面提到了PHP的运行机制，不同于长时间运行的语言或系统，PHP很少会在不同的php-fpm进程中共享实体，最多也只是在同一次请求中共享。  </p>
<p>这样，当我们在一次请求中需要处理两个或两个以上的用户实体时，可以怎么应对呢？<br />
关于对实体的追踪和识别，可以使用ORM进行实体与关系数据库映射，但PhalApi弱化了这种映射，取而代之的是更明朗的处理方式，即： <strong>无状态操作</strong> 。 </p>
<p>因为PhalApi都是通过“空洞”的实体来获得数据，即实体无内部属性，对数据库的处理采用了 <strong>表数据入口模式</strong> 。<br />
当我们需要获取两个用户的信息时，可以这样：</p>
<pre><code class="language-javascript">$model = new Model_User();
$user1 = $model-&gt;get(1);  //$user1是一个数组
$user2 = $model-&gt;get(2);

//而不是
$user1 = new Model_User(1);  //$user1是一个对象
$user2 = new Model_User(2);

//或者可以这样批量获取
$users = $model-&gt;multiGet(array(1, 2));  //$users是一个二维数组，下标是用户的ID</code></pre>
<p>这样做，没有绝对的对错，可以根据你的项目应用场景作出调整。但我觉得无状态在PhalApi应用，可以更简单便捷地处理各种数据以及规则的统一，以实现操作的无状态。因为：  </p>
<ul>
<li>1、可以按需取得不同的字段，多个获取时可以使用批量获取</li>
<li>2、在单次请求处理中，简化对实体的追踪和维护</li>
<li>3、换种方式来获得不可变值性的好处，因为既然没有内部状态，就没有改变了</li>
</ul>
<h3>(4)引申到Domain层</h3>
<p>Domain层作为ADM（Api-Domain-Model）分层中的桥梁，主要负责处理业务规则。  </p>
<p>将值对象与无状态操作引申到Domain层，同样有处于简化我们对数据和业务规则的处理。  </p>
<p>我们可以根据上述的家庭组成员领域类来完成类似下面功能场景的业务需求：</p>
<pre><code class="language-javascript">$domain = new Domain_Group_Member();

if (!$domain-&gt;hasJoined(1, 100)) {
    $domain-&gt;joinGroup(1, 100);
}
if (!$domain-&gt;hasJoined(2, 100)) {
    $domain-&gt;joinGroup(2, 100);
}
if (!$domain-&gt;hasJoined(3, 100)) {
    $domain-&gt;joinGroup(3, 100);
}</code></pre>
<p>即：如果用户1还没加入过组100，那么就允许他加入。用户2、用户3也以此类推。  </p>
<p>当我们把业务规则，划分为更细的维度时，我们可以轻松上在业务层组装不同的功能，讲述不同的故事。</p>
<h2>2.16.5 越痛苦的事情，越早做</h2>
<p>在有一次敏捷开发分享会上，有位前辈说：要对小问题不断进行优化迭代，而不要等到大问题来了再作变革。<br />
也就是我们常听说的黄金法则： <strong>演变优于剧变</strong> 。  </p>
<p>同样，在持续集成中，也提倡着类似的理念，即：越痛苦的事情，越早做。</p>
<p>有时，对自己狠一点，是会有所收获的。  </p>
<p>我们都赞扬美好的事物，但我们很少也会那样去做，因为我们知道美好需要付出更多的努力，意味着有所牺牲。  </p>
<p>如很多女生，都很喜欢苗条的身材，却总忍不住零食的诱惑，也很难坚持锻炼。<br />
我们很多人都喜欢优雅的代码，自已却也会写下一些临时性的代码，而没多及时清理代码的异味，也没有尝试去重构，更没有坚持单元测试。  </p>
<p>从另外一个角度说，如果一个项目的问题，我们在前期及时沟通并解决的话，根本不值得过多去关注。但若我们因为团队关系或者心烦意乱有意识去抵触多变的需求时，一个很小的问题，到了上线后，就可能会演变成一个灾难。<br />
到了那时，即使只是一行代码的改变，也会涉及到开发、测试、产品、运维、用户、商务、老板等等一系列的干涉人。为了修复上线，我们还要走一系列的发布流程，事后还需要为这样的故障买单。  </p>
<p>既然如此，明明知道当初一个不确定的需求时，为什么没去及时处理呢？<br />
我知道，我相信我们大家也知道，程序员总会有一些很烦很想抵触的时候，这时我们会拒绝改变，拒绝去做一些正确的事情。  </p>
<p>但越痛苦的事情，越早做，不仅仅需要我们增强对自身的情商控制，更让我们能很好地应对高价值的系统。<br />
试想，谁会把一个影响到千千万万用户、涉及到动则百万金额的系统交给一个动不动就发脾气的人呢？  </p>
<p>所以，对自己“狠”一点吧，明天的你，将会感谢今天努力的你。</p>
<h2>2.16.6 收篇</h2>
<p>领域驱动设计所提供的思想、概念和设计非常广泛，这里不能一一说明。本章仅仅是摘取其中的部分内容进行再传播，以唤醒入门同学的注意，培养一种约定编程的意识。 </p>
<p>更为重要的是，要懂得去学习，学习后应用到自己当前的工作或项目中，慢慢地你将体会到开发编程的乐趣。  </p>
<hr />
<h4><a href="/wikis/%5B2.15%5D-%E6%BC%94%E8%BF%9B%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%E7%BB%AD%E7%AF%87.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.17%5D-%E5%BE%AE%E6%9C%8D%E5%8A%A1%EF%BC%9AApi%E6%8E%A5%E5%8F%A3%E6%9C%8D%E5%8A%A1%E5%B1%82.html">下一章</a>#[2.17]-微服务：Api接口服务层</h4>
<h4><a href="/wikis/%5B2.16%5D-%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%BA%94%E5%AF%B9%E5%A4%8D%E6%9D%82%E9%A2%86%E5%9F%9F%E4%B8%9A%E5%8A%A1%E7%9A%84Domain%E5%B1%82-.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a></h4>
<hr />
<p><em>我发现，我越是努力，就越发幸运。 -- Thomas Jefferson</em></p>
<h2>2.17.1 微服务</h2>
<p><img src="http://7qnay5.com1.z0.glb.clouddn.com/qq_20150607110405.jpg" alt="a pic" />  </p>
<p>Martin Fowler（我喜欢和敬仰的大师）曾发表了上面这一段话。这段话也出现在了2015年QCon分享会上，并加了一张PPT“什么是微服务”加以说明。 </p>
<p><img src="http://7qnay5.com1.z0.glb.clouddn.com/qcon_20150607.png" alt="a pic" />   </p>
<p>里面提到了 <strong>微服务</strong> 这个概念，在PhalApi框架中即对应我们的Api接口服务层，只是我们不是称之为微服务，而是接口服务。<br />
不管何种说法，我们都应该关注里面提及到的这几点重要特质：  </p>
<ul>
<li>小，且专注于做一件事情</li>
<li>独立的进程中</li>
<li>轻量级的通信机制</li>
<li>松耦合、独立部署</li>
</ul>
<p>这里不过多地讨论微服务相关的分享，而是重温接口服务层Api与领域驱动和单元测试之间的关系，以及如何开发一个优雅、稳定又简单的接口。  </p>
<h2>2.17.2 层级调用的顺序</h2>
<p>整体上讲根据从Api接口层、Domain领域层再到Model数据源层的顺序进行开发。  </p>
<p>在开发过程中，需要注意不能 <strong>越层调用</strong> 也不能 <strong>逆向调用</strong> ，即不能Api调用Model。而应该是 <strong>上层调用下层，或者同层级调用</strong> ，也就是说，我们应该：</p>
<ul>
<li>Api层调用Domain层</li>
<li>Domain层调用Domain层</li>
<li>Domain层调用Model层</li>
<li>Model层调用Model层</li>
</ul>
<p>如果用一张图来表示，则是：<br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/201506071124.png" alt="a pic" />  </p>
<p>为了更明确调用的关系，以下调用是 <strong>错误</strong> 的：  </p>
<ul>
<li>错误的做法1：Api层直接调用Model层</li>
<li>错误的做法2: Domain层调用Api层，也不应用将Api层对象传递给Domain层</li>
<li>错误的做法3: Model层调用Domain层 </li>
</ul>
<p>这样的约定，便于我们形成统一的开发规范，降低学习维护成本。<br />
比如需要添加缓存，我们知道应该定位到Model层数据源进行扩展；若发现业务规则处理不当，则应该进入Domain层探其究竟；如果需要对接口的参数进行调整，即使是新手也知道应该找到对应的Api文件进行改动。  </p>
<h2>2.17.3 接口服务的定义</h2>
<p>现实项目开发过程中，绝大部分我们编写的接口是给别人使用的，或许给Android客户端同学使用，或者给iOS客户端同学使用，抑或提供给其他后台系统的同学使用。<br />
为了提高并行开发的速度，我们不能等待接口完全开发完成后才提供接口文档，而且他们也不能忍受这么漫长的等待。   </p>
<p>所以，客户端同学时常会问我们：什么时候可以提供接口文档？  </p>
<p>我们提倡“接口先行”，如果有时不能很好地做到这一点（毕竟多变的需求促发多变的情境），我们可以快速提供接口的定义。<br />
这有点像规约层对接口的定义一样，在PhalApi中定义一个接口，再具体一点即：  </p>
<ul>
<li>1、创建一个接口服务类和声明函数</li>
<li>2、配置接口参数规则</li>
<li>3、提供接口返回格式的注释</li>
</ul>
<p>简单来说，就是创建一个类，写个函数，定义参数和返回结果。  </p>
<p>下面以 <a href="http://git.oschina.net/dogstar/PhalApi-Demo-Vote">开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）</a> 中的团队参赛接口为例，说明这三步操作的过程。  </p>
<h3>(1)创建一个接口服务类和声明函数</h3>
<pre><code class="language-javascript">//$ vim ./Vote/Api/Act.php 

&lt;?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }
}</code></pre>
<h3>(2)配置接口参数规则</h3>
<pre><code class="language-javascript">&lt;?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' =&gt; array(
                'teamName' =&gt; array('name' =&gt; 'team_name', 'require' =&gt; true, 'min' =&gt; 1, 'max' =&gt; 100),
            ),
        );
    }

    public function joinIn() {
    }
}</code></pre>
<h3>(3)提供接口返回格式的注释</h3>
<pre><code class="language-javascript">&lt;?php
class Api_Act extends PhalApi_Api {

    public function getRules() {
        return array(
            'joinIn' =&gt; array(
                'teamName' =&gt; array('name' =&gt; 'team_name', 'require' =&gt; true, 'min' =&gt; 1, 'max' =&gt; 100),
            ),
        );
    }

    /**
     * 团队参赛接口
     *  
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
    }
}</code></pre>
<h3>(4)在线查看一下效果</h3>
<p>在完成上面的动作后，我们可以通过在线工具来看下实时的效果，在浏览打开后访问：</p>
<pre><code>http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn</code></pre>
<p>可以看到：<br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/201506071302.jpg" alt="a pic" />  </p>
<p>到了这里，即使我们未完成接口的开发，也未提供更完善的接口文档，但接口客户端同学也可以根据这个在线的接口参数进行开发了。</p>
<h2>2.17.4 在ATDD下讲述故事</h2>
<p>我们一直推崇测试驱动开发，但在对于Api接口开发，更准确来说是ATDD，即：验收测试驱动开发（Acceptance Test Driven Development）。  </p>
<p>在前面Domain层文档中，我们提到了Api层是讲述故事的场景。因此，为了验证我们的业务场景是否正确，我们应该事先编写好单元测试，以不断引导我们前往正确的目的地。  </p>
<p>我们可以使用脚本来快速生成测试骨架：</p>
<pre><code class="language-javascript">$ pwd
$ /path/to/api.vote.phalapi.com/Vote/Tests/Api
$ phalapi-buildtest ../../Api/Act.php Api_Act ../test_env.php &gt; Api_Act_Test.php</code></pre>
<p>然后，稍微修改完善测试场景：</p>
<pre><code class="language-javascript">    /**
     * @group testJoinIn
     */
    public function testJoinIn()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.JoinIn';
        $params = array(
            'sign' =&gt; 'phalapi',
            'team_name' =&gt; 'test team name',
            'user_id' =&gt; '1',
            'token' =&gt; '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731',
        );

        DI()-&gt;notorm-&gt;team-&gt;where('team_name', $params['team_name'])-&gt;delete();

        //Step 2. 执行请求  
        $rs = PhalApiTestRunner::go($url, $params);
        //var_dump($rs);

        //Step 3. 验证
        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('team_id', $rs);
        $this-&gt;assertEquals(0, $rs['code']);
        $this-&gt;assertGreaterThan(0, $rs['team_id']);

        //create again
        $rs = PhalApiTestRunner::go($url, $params);
        $this-&gt;assertEquals(1, $rs['code']);
    }
</code></pre>
<p>从上面测试的代码可以看出，我们先后进行了两次报名。明显地，第一次报名应该是成功的，第二次则应该提示不能重复报名。  </p>
<p>单元测试的好处，不但在于可以引导我们做正确的事情，还可以提高我们的关注点，不致于在开发过程中被各种事务（如临时性的会议）打断后回来却不知刚才开发到哪了。<br />
然而，更多的是为后期的维护、扩展提供可验证的业务场景。这点是很重要的。因为每一个测试场景，都保存了对应场景的模拟信息，这样不仅仅在后面的扩展，还是突如其来的BUGFIXED，我们都可以快速证明我们的修改是正确的，至少不会影响到原来的业务流程。<br />
试想一下，如果原来可以下单支付的接口，突然被影响到而导致支付不成功，这是何等的损失！  </p>
<p>我现在慢慢地，每当需要修改别人的代码时，我都会看下有没有对应的单元测试。如果没有，我会先补回，这样能增强我修改别人代码的信心。  </p>
<h2>2.17.5 精益接口开发</h2>
<p>传统的接口开发，由于没有很好的分层结构，而且热衷于在一个文件里面完成绝大部分事情，最终导致了臃肿漫长的代码，也就是通常所说的意大利面条式的代码。  </p>
<p>在PhalApi中，我们针对接口领域开发，提供了新的分层思想：ADM（Api - Domain - Model）。<br />
即便这样，如果项目在实际开发中，仍然使用原来的做法，纵使使用再好的接口开发框架，也还是会退化到原来的局面。  </p>
<p>为了能让大家更为明确Api接口层的职责所在，我们建议：  </p>
<h3>(1)Api接口层应该做：</h3>
<ul>
<li>应该：对用户登录态进行必要的检测</li>
<li>应该：控制业务场景的主流程，创建领域业务实例，并进行调用</li>
<li>应该：进行必要的日记纪录</li>
<li>应该：返回接口结果</li>
</ul>
<h3>(2)Api接口层不应该做：</h3>
<ul>
<li>不应该：进行业务规则的处理或者计算</li>
<li>不应该：关心数据是否使用缓存，或进行缓存相关的直接操作</li>
<li>不应该：直接操作数据库</li>
<li>不应该：将多个接口合并在一起</li>
</ul>
<p>在明确了上面应该做的和不应该做的，并且也完成了接口的定义，还有验收测序驱动开发的场景准备后，相信这时，即使是新手也可以编写出高质量的接口代码。<br />
因为他会受到约束，他知道他需要做什么，主要他按照限定的开发流程和约定稍加努力即可。  </p>
<p>如果真的这样，相信我们也就慢慢能体会到 <strong>精益开发</strong> 的乐趣。  </p>
<p>最后，让我们一起来看下上述团队参赛接口开发的代码实现：</p>
<pre><code class="language-javascript">    /**
     * 团队参赛接口
     *  
     * @return int code 0，参赛成功；1，队名已存在
     * @return int team_id 新建的团队ID
     */
    public function joinIn() {
        $rs = array('code' =&gt; 0, 'team_id' =&gt; 0);

        DI()-&gt;userLite-&gt;check(true);

        $domain = new Domain_Team();
        if ($domain-&gt;isExists($this-&gt;teamName)) {
            $rs['code'] = 1;
            return $rs;
        }

        $teamId = $domain-&gt;joinIn($this-&gt;teamName);
        $rs['team_id'] = $teamId;

        return $rs;
    }
</code></pre>
<p>可以看出，上面的代码短小达意，简单清晰。</p>
<hr />
<h4><a href="/wikis/%5B2.16%5D-%E9%A2%86%E5%9F%9F%E9%A9%B1%E5%8A%A8%E8%AE%BE%E8%AE%A1%EF%BC%9A%E5%BA%94%E5%AF%B9%E5%A4%8D%E6%9D%82%E9%A2%86%E5%9F%9F%E4%B8%9A%E5%8A%A1%E7%9A%84Domain%E5%B1%82-.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a>#[2.19]-扩展库：可重用的扩展类库</h4>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.20%5D-%E7%BA%A6%E5%AE%9A%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%9E%B6%E6%9E%84%E6%98%8E%E6%98%BE%E7%9A%84%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC.html">下一章</a></h4>
<hr />
<h2>PhalApi框架扩展类库</h2>
<h3>-- 致力于与开源项目一起提供企业级的解决方案！</h3>
<p>此部分类库为 <a href="http://git.oschina.net/dogstar/PhalApi">PhalAPi框架</a> 下的扩展类库包，各个扩展包各自独立，可以根据需要自动下载安装。</p>
<p>虽然此部分的扩展很多都是基于已有的第三方开源实现，或者开发同学自己实现分享的，但我们所做的不只是代码的搬运工，更多的是在系统架构上的组件库重用，以及在此引入 <strong>防腐层</strong> ，避免进入 <strong>供应商锁定（Vendor Lock-In）</strong> 。  </p>
<p>除此之外，可以在PhalApi框架的基础上，快速引入符合我们国内实际项目开发需要的各种扩展。如时，您会发现，原来编程本来就是一件如此简单的事情，就像搭积木一样。  </p>
<p>正如我们一直推荐的：</p>
<pre><code>接口，从简单开始！</code></pre>
<h2>目前已提供的扩展类库 - 针对使用者</h2>
<h3>1、微信开发</h3>
<p>此扩展可用于微信的服务号、订阅号、设备号等功能开发，则PhalApi框架下简单配置即可开发使用。  </p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91">更多请点这里查看使用文档</a></p>
<h3>2、代理模式下phprpc协议的轻松支持</h3>
<p>此扩展可用于phprpc协议的调用，服务端只需要简单添加入口即可完美切换。  </p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81">更多请点这里查看使用文档</a></p>
<h3>3、基于PHPMailer的邮件发送</h3>
<p>此扩展可用于发送邮件。  </p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81">更多请点这里查看使用文档</a>  </p>
<h3>4、优酷开放平台接口调用</h3>
<p>此扩展可用于调用优酷开放平台的接口。</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8">更多请点这里查看使用文档</a>  </p>
<h3>5、七牛云存储接口调用</h3>
<p>此扩展可以用于将图片上传到七牛CDN，或者其他七牛接口的调用。  </p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8">更多请点这里查看使用文档</a></p>
<h3>6、用户、会话和第三方登录集成</h3>
<p>此类库主要特点有：</p>
<ul>
<li>1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录</li>
<li>2、为客户端提供了直接可以调用的登录接口</li>
<li>3、为服务端提供了直接可以检测用户登录态的操作</li>
<li>4、支持token落地、高效缓存和分布式的数据库存储  </li>
<li>
<p>5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.8%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E7%94%A8%E6%88%B7%E3%80%81%E4%BC%9A%E8%AF%9D%E5%92%8C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%BD%95%E9%9B%86%E6%88%90">更多请点这里查看使用文档</a></p>
</li>
</ul>
<h3>7、swoole支持下的长链接和异步任务实现</h3>
<p>目前，此扩展类库提供了：</p>
<ul>
<li>长链接的接口调用</li>
<li>
<p>异步计划任务的调用</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.9%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9Aswoole%E6%94%AF%E6%8C%81%E4%B8%8B%E7%9A%84%E9%95%BF%E9%93%BE%E6%8E%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E5%AE%9E%E7%8E%B0">更多请点这里查看使用文档</a></p>
</li>
</ul>
<h3>8、新型计划任务</h3>
<p>此扩展类型用于后台计划任务的调度，主要功能点有：</p>
<ul>
<li>1、提供了Redis/文件/数据库三种MQ队列</li>
<li>2、提供了本地和远程两种调度方式</li>
<li>3、以接口的形式实现计划任务</li>
</ul>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.6%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1">更多请点这里查看使用文档</a></p>
<h3>9、Auth 权限扩展 (由@黄苗笋提供)</h3>
<p>实现了基于用户与组的权限认证功能，与RBAC权限认证类似，主要用于对服务级别的功能进行权限控制，主要功能点有：</p>
<ul>
<li>1、提供了接口服务维度的权限验证</li>
<li>2、提供了可配置的组与规则</li>
<li>3、支持免检用户</li>
</ul>
<p><a href="http://git.oschina.net/dogstar/PhalApi-Library/wikis/Auth-%E6%9D%83%E9%99%90%E6%89%A9%E5%B1%95%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3">更多请点这里查看使用文档</a></p>
<h3>11、FastRoute快速路由</h3>
<p>此扩展基于 <img src="https://github.com/nikic/FastRoute" alt="FastRoute" /> 实现，需要 <strong>PHP 5.4.0</strong> 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。，主要有：</p>
<ul>
<li>1、基于FastRoute实现</li>
<li>2、需要PHP 5.4.0 及以上版本  </li>
<li>3、通过配置文件来实现自定义路由，并映射到service</li>
<li>
<p>4、可兼容无路由的历史URI</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi/wikis/%5B3.11%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EFastRoute%E7%9A%84%E5%BF%AB%E9%80%9F%E8%B7%AF%E7%94%B1">更多请点这里查看使用文档</a></p>
</li>
</ul>
<h3>12、基于PhalApi的DB集群拓展DB_Cluster (由@喵了个咪提供)</h3>
<p>为应对海量数据分析与统计，提供针对分表分库统一封装的数据库操作接口，主要用于解决大量数据写入分析的问题。请注意：V0.1bate版本,很多功能尚不完善,只提供技术交流使用,请不要用于生产环境。主要特点有：</p>
<ul>
<li>1、适用于大量select和大量的insert的业务场景</li>
<li>2、基于架构思维的实现</li>
<li>3、分表分库算法介绍</li>
<li>
<p>4、基准测试对比</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi-Library/tree/master/DB_Cluster/?dir=1&amp;filepath=DB_Cluster&amp;oid=a50865e4e86d8105bdc30c12a8193db1f119cdb5&amp;sha=8c16393c1286f3921cb920dd21a4aab5eb05f8a3">更多请点这里查看使用文档</a></p>
</li>
</ul>
<h3>13、基于PhalApi的Redis_Base拓展 (由@喵了个咪提供)</h3>
<p>主要提供更丰富的Redis操作,并且进行了分库处理可以自由搭配</p>
<ul>
<li>1、适用于对Redis需要其他数据类型操作的业务</li>
<li>
<p>2、可以用于队列脚本,封装了队列处理</p>
<p><a href="http://git.oschina.net/dogstar/PhalApi-Library/wikis/%E5%9F%BA%E4%BA%8EPhalApi%E7%9A%84Redis%E6%8B%93%E5%B1%95--%E6%96%87%E6%A1%A3">更多请点这里查看使用文档</a></p>
</li>
</ul>
<h2>扩展开发指南 - 针对开发者</h2>
<p>为了统一扩展类库的风格、便于用户更容易使用，这里建议：  </p>
<ul>
<li>代码：统一放置在Library目录下，各扩展包各自一个目录，尽量Lite.php入口类，遵循PEAR包命名规范；</li>
<li>配置：统一放置在DI()-&gt;config-&gt;get('app.扩展包名')中，避免配置冲突；</li>
<li>文档：统一提供WIKI文件对扩展类库的功能、安装和配置、使用示例以及运行效果进行说明；</li>
</ul>
<h2>1.19.4 从微架构到扩展类库的演进</h2>
<p>在应用项目的实际开发，我们也可以有意识地将一些通用的工具和操作与业务分离，以便可以在项目内更好地重用。当抽离成工具或者通用类后，则可以进一步推广到公司内其他项目，即组件复用。如果觉得可以，则发扬开源精神，分享给社区。这也是符合从微架构到应用构架、系统架构、乃至企业架构的演进之路。</p>
<hr />
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.20%5D-%E7%BA%A6%E5%AE%9A%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%9E%B6%E6%9E%84%E6%98%8E%E6%98%BE%E7%9A%84%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC.html">下一章</a>#[2.20]-约定编程：架构明显的编程风格</h4>
<h4><a href="/wikis/%5B2.19%5D-%E6%89%A9%E5%B1%95%E5%BA%93%EF%BC%9A%E5%8F%AF%E9%87%8D%E7%94%A8%E7%9A%84%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.21%5D-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%9F%E4%B8%80%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E7%AE%80%E6%98%8E%E7%89%88%EF%BC%9ACentOs---Nginx---php-fpm---MySql-%5B--Memcached%5D.html">下一章</a></h4>
<hr />
<h2>2.20.1 前言</h2>
<p>我们一直强调推荐让后台接口开发更简单。  </p>
<p>所以，我们提供了PhalApi开发框架和WIKI文档。然后，这仅仅是个开始。  </p>
<p>因为，真正发挥作用，实现价值的还是来自项目实际开发中的源代码。  </p>
<p>但纵使我们提供了好的框架作为基础，但不得不承认的一个事实是：架构模型和项目代码随着各自的演进逐渐产生分歧。而这种分歧，如果没有注意、管理和约束，则会产生越来越混乱的代码。  </p>
<p>在说明如何编写简洁的项目代码之前，让我们先了解一下PhalApi的架构思想以及主要的设计意图。</p>
<h2>2.20.2 开发-配置-使用 模式 (develop-config-use pattern)</h2>
<p>开发-配置-使用 模式即：开发实现-配置注册-客户使用 模式。  </p>
<p>现分说如下。  </p>
<h3>(1)开发实现</h3>
<p>开发实现的主要内容是组件、公共服务或者基础设施的功能实现，此部分主要针对高级开发工程师，或者有经验的PHP同学。  </p>
<p>例如对项目的接口签名的验证拦截、一个完成了对七牛云存储接口调用的扩展、又或者是项目内部加密的方案等，这些以包或者接口提供，为外部使用提供了配置说明、使用示例和文档说明。更为重要的是，应该提供了配套的单元测试，并有着很高的代码覆盖率。  </p>
<p>此类实现应该是稳定的，即没有明显或者隐藏的BUG。即使有，原作者也可以快速进行定位和解决，以及后期的扩展和升级。</p>
<h3>(2)配置注册</h3>
<p>一旦上面的接口被实现后，不同的项目都可以轻松引入和使用。这块通常由项目的负责人，或者主程来操作，因为在对项目进行构建部署、组件和环境装配时，需要考虑到哪些组件需要被用到，以何种方式进行初始化和装载。    </p>
<p>但使用的方式，应该是简明的。如简明的安装，简明的配置。所以，这里自然而言，就涉及到了 <strong>依赖注入</strong> ，DI。  </p>
<p>通过DI，项目的负责人，可以轻松地将已通过严格测试的组件/服务注册进来。完成此步骤后，一切都整装待发，剩下的就是使用的问题了。</p>
<h3>(3)客户使用</h3>
<p>项目会不断有新的需求出来，而团队也会因此同步增加吸纳新开发同学进来负责新模块新功能的开发。而新的同学，往往会是一些开发新手，他们需要使用已有的功能，快速实现一些具体的业务逻辑、规则和功能。  </p>
<p>但如果他们还需要实现一些基础重要的功能，又要考虑如何与现在项目整合，会分散他们的关注点。而且，即使放手给他们去做，他们也会常常因为考虑不周或者编程风格各异而产出一些与项目期望不符的代码。  </p>
<p>若换一种工作的方式，即如果新手使用已有的组件进行一些特定领域业务的开发，会是怎样？ </p>
<p>我想，会有很大的改观。  </p>
<p>比如，我们对新来的同学说，你使用DI()-&gt;logger就可以写一条日记了，如：</p>
<pre><code class="language-javascript">DI()-&gt;logger-&gt;debug('app enter');</code></pre>
<p>新手可能很喜欢追问一些问题，他可能会问及到，那怎么将一些参数（当时日记的上下文）也进行纪录呢？你可以很骄傲地说：也是可以的，你只需要这样写就可以了：</p>
<pre><code class="language-javascript">DI()-&gt;logger-&gt;debug('app enter', array('device' =&gt; 'iOS', 'version' =&gt; '1.1.0'));</code></pre>
<h3>(4)创建和使用分离</h3>
<p><strong>开发-配置-使用 模式</strong> 也符合了创建和使用分离的思想。  </p>
<p>不同的项目，不同的应用，需要的初始化服务不一样；不同的规模，对不同的技术解决方案也不一样；不同的环境，配置也不一样。  </p>
<p>但即使是这样，新手还是可以一如既往地使用之前注册的服务（也就是不需要修改任何调用代码）。也就是上层的调整或者环境变更这些，对新手的使用都是透明的。为了更好地理解这些概念，这里补充一些案例场景。    </p>
<h4>以日记为例</h4>
<p>假设我们有个项目A，分别部署到内网测试环境和外网生产环境，显然内外网环境的配置是不一样的。我们希望在内网环境为日记开启debug模式以方便开发人员进行调试，在外网则希望将其关闭以减少系统的性能开销。在一开始使用文件作为日记存储方案时，对应的内网环境初始化代码如下：  </p>
<pre><code class="language-javascript">//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', 
    PhalApi_Logger::LOG_LEVEL_DEBUG | PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);</code></pre>
<p>在外网，我们只需要去掉PhalApi_Logger::LOG_LEVEL_DEBUG即可：</p>
<pre><code class="language-javascript">//日记纪录
DI()-&gt;logger = new PhalApi_Logger_File(API_ROOT . '/Runtime', 
    PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);</code></pre>
<p>随着项目的不断发展，我们有了一批又一批的新用户。产品经理为此很开心，也请我们开发吃了好几顿大餐。但谨慎的我们发现了现在文件日记的一些限制。如即时文件读写带来了I/O瓶颈，而且不能将分布式的日记文件自动收集起来。所以，我们决定对logger进行更深层次的探索。。。  </p>
<p>至于最后是使用了Hive还是Hadoop，还是MC异步后台队列的方式实现，我们这里不具体指定。假设新的logger研发成功后，我们便可以轻松对原有的文件日记组件进行升级，实现完美切换：</p>
<pre><code class="language-javascript">//升级后的日记纪录
DI()-&gt;logger = new My_Logger(PhalApi_Logger::LOG_LEVEL_INFO | PhalApi_Logger::LOG_LEVEL_ERROR);</code></pre>
<p>这不仅是几行代码上的区别，而是针对不同问题不同技术解决方案的抉择。这也是有经验的开发和新手之间的区别，因为你选择的技术解决方案要和面临的风险相匹配。例如用牛刀来杀鸡，就是一个不匹配的做法，就如同使用高级的Hive来实现单一小项目的日记存储一样。  </p>
<p>这是令人值得兴奋的。在很多旧的项目里面，当遇到瓶颈时，会请一些外部的专家来指导或优化。但即使拥有着各种“法宝”以及知道何时该使用哪种方案的专家，对于这种残留的代码也会步履维艰基于束手无策。因为，各种初始化和调用的代码，分遍在项目的“全国各地，四面八方”。即使你优化了，你会发现还要手动一个个地进行切换升级。更重要的是，很多时候不是你想优化就能优化的。  </p>
<p>我曾经遇到过这样一个旧系统。它是在UcHome基础上做二次开发，但对于它的数据库使用，开发人员没有过多地优化，如：没有使用缓存，没有进行批量合并查询优化，重复查询相同的数据，没有建立索引等等，等等。这样的后果就是，请求一次接口，会触发150条到500条SQL语句不等。我后来，在底层添加了在线查看调试SQL语句的功能，尝试进行了一些合并查询，但当我想为数据库的表添加索引时，发现它用的却是虚拟表 -- 视图！</p>
<h3>(5)扩展类库对“开发-配置-使用”模式的应用</h3>
<p>如果说DI服务是微观上对“开发-配置-使用”模式的使用，那么PhalApi的扩展类库则是宏观上的应用。  </p>
<p>扩展类库也是由第三方（可能是PhalApi开发团队、他人或者你）开发实现的，然后再通过简单配置（或者免配置），就可以使用扩展类库的功能了。如邮件发送、phprpc协议。<br />
之所以提供扩展类库的形式，是因为DI服务更适合于单个类以及几个操作接口，而扩展类库则提供更丰富的功能操作和一系列的接口。  </p>
<p>这样以后，项目就可以简单快速共享各种扩展类库了。难道这不是一件令人兴奋的事情吗？<br />
因为“哈啊！我又找到了一个可以直接用的代码类库”，而不是“唉，又要写一堆代码，还要测试、联调。。。”。  </p>
<h3>(6)回顾Yii框架的发现</h3>
<p>程序、系统和框架，其作用太多数都体现在动态的功能上，而不是静态有限的功能。而动态的功能则很大程序上依赖于各种配置，如Tomcat下各层级xml配置。有些框架对配置这块提供了丰富的支持，但为此的代码是，配置难以掌控。  </p>
<p>就拿Yii框架为例（Yii确实是一个很了不起的框架，这里只是以事论事），当你需要在视图渲染一个数据表格时，你可以使用 <a href="http://www.yiichina.com/doc/api/1.1/CGridView">CGridView</a> ，并类似这样配置：</p>
<pre><code class="language-javascript">$columns = array(
    array('name' =&gt; 'mId', 'header' =&gt; '序号'),
    array('name'=&gt;'id', 'header'=&gt;'事件ID'),
    array('name'=&gt;'title', 'header'=&gt;'标题'),
    array('name'=&gt;'content', 'header'=&gt;'内容', 'type' =&gt; 'html'),
);

$this-&gt;widget('bootstrap.widgets.TbGridView', array(
    'type'=&gt;'striped bordered condensed',
    'dataProvider'=&gt;$dataProvider,
    'columns'=&gt; $columns,
));</code></pre>
<p>更为复杂的情况可以是：</p>
<pre><code class="language-javascript">$columns = array(
    // ...
    array('class' =&gt; 'CDataColumn', 'header' =&gt; '内容', 'type' =&gt; 'html', 'name' =&gt; 'content', 'htmlOptions' =&gt; array('width' =&gt; '200px')),
    array(
        'class'=&gt;'CButtonColumn',
        'template'=&gt;'{showEvent}&lt;br/&gt;&lt;br/&gt;{deleteEvent}',
        'header'=&gt;'操作',
        'buttons'=&gt;array
        (
            'showEvent' =&gt; array(
                'label' =&gt; '查看',
                'url' =&gt; '"?r=DailyOperations/eventManagerShow&amp;user_iduser_id=' . $userId  . '&amp;eventId=". $data["id"];',
                'options' =&gt; array('target' =&gt; '_blank'),
            ),
            'deleteEvent' =&gt; array(
                'label'=&gt;'删除',
                'url'=&gt;'"javascript:void(0)"',
                'imageUrl'=&gt;'/images/delete_24.png',
                'deleteConfirmation'=&gt;"js:'Record with ID '+$(this).parent().parent().children(':first-child').text()+' will be deleted! Continue?'",
                'click'=&gt;'js:function(){if (confirm("此操作将删除：ID = " + $(this).parent().parent().children(\':first-child\').text() + " \n是否确定？")) {deleteEvent($(
this).parent().parent().children(\':first-child\').text());};}',
            ),
        ),
    ),
);

// ...</code></pre>
<p>然后，对于我这么笨的人来说，不管是简单的配置，还是复杂的配置，每次当我需要使用时，我都非常害怕且需要从以下三方便获取帮助：</p>
<ul>
<li>找曾经写过类似的代码并拷贝过来修改；</li>
<li>“耐心”（耐着心）查看官方的文档；</li>
<li>网上搜索相关的例子</li>
</ul>
<p>因为，每次我都记不住这些配置，但我又不得不承认它的效果很好。然后我觉得其缺点至少有两点：</p>
<ul>
<li>缺点1：尽管是很简单的功能也需要用配置来实现，从而导致配置羞涩难懂；</li>
<li>缺点2：配置太复杂，对人的记忆要求太高；</li>
</ul>
<p>这是我对Yii框架配置的体会。</p>
<h5>自己工作的回顾</h5>
<p>最初，感受到配置式的开发，是在大学的时候做一个OutLook的插件。这个插件需要同步本地和远程服务器的联系人，其中当有冲突时，就有这么几种策略：冲突时以本地为准、冲突时以远程为准、冲突时提醒我、忽略冲突。 </p>
<p>这是当时写的博客，感兴趣可以看看 <a href="http://my.oschina.net/dogstar/blog/95314">配置编程： 让项目开发从多样到统一 </a>   </p>
<p>这有点像我们常用的SVN的处理方式。然而当我在尝试开发实现时，我发现过程很复杂，但处理又是如此相似。这里的区别很微妙，特别这些策略又是由外部用户指定时。最后，我惊讶地发现，如果我使用配置来做的话，会非常简单且明了！  </p>
<p>但那时，只是初体会。  </p>
<p>现在，经过了几年的开发，我才慢慢发现，可以把这种开发模式总结为：开发-配置-使用模式。<br />
不知是否有其他模式和此新发现的模式类似？</p>
<h2>2.20.3 框架外延</h2>
<p>框架外延是指跨项目，与业务无关的抽象特性与代码实现。  </p>
<p>这些从简单到复杂，有：通用功能，定制，扩展类库和产品簇框架。  </p>
<h3>(1)通用功能</h3>
<p>有些功能是常用的，也是通用的。一般以函数或者工具类的形式提供，如一个生成随机数的方法。这些通用的功能，开发人员都可以在项目间流通使用。  </p>
<h3>(2)定制</h3>
<p>当发现PhalApi现在的框架不支持某些接口下具体方案的实现时，或者支持但不满足当前项目的开发需要时，可以进行一些定制化。  </p>
<p>之前有一个项目的开发同学，给我提供了一个很好的建议。他说他的项目为了能够获得更高的安全性，对客户端传递的参数进行了整包加密，然后在服务商进行整包解密再通过getRules()获取。所以对应的定制大概实现可以是这样的：</p>
<pre><code class="language-javascript">&lt;?php
class My_Request extends  PhalApi_Request {

     protected function genData($data){
         $needData = array();

        if (!isset($data) || !is_array($data)) {
            $data = $_POST;
        }

        $needData['service'] = isset($data['service']) ? $data['service'] : '';
        //整包加密后的数据包
        $params = isset($data['params']) ? $data['params'] : '';
        //TODO: 对称解密 ...

        $needData = array_merge($params, $needData);

        return $needData;
    } 
}</code></pre>
<h3>(3)扩展类库</h3>
<p>有时候，我们在项目中需要使用到一组功能操作，这时可以结合外观模式，将这些需要用到的接口以包的形式都封装到一个扩展里面。因为扩展这个概念是容易理解的，也是开发人员所喜爱的。  </p>
<p>在PhalApi的Issues里面，有位同学就提到了对新浪或者百度云空间的支持，就是对应的一个场景。  </p>
<p>现在PhalApi提供了部分的扩展类库，但由于个人时间有限，而且也不可能知悉全部项目需要用到的全部扩展。所以当发现需要的扩展类库还没有提供时，简单的方法：自动写一个。  </p>
<p>这也是对框架进行外延的一种途径。伴随着这种途径经历的次数越多，你会突然有一天发现你所搭建的这一切结合起来后，已经可以很好地应对了公司内的目前全部项目的开发。</p>
<h3>(4)产品簇框架</h3>
<p>这个时候，就来了产品簇框架这一阶段。  </p>
<p>PhalApi只是提供了一个基础的框架，一个项目实际开发的基础框架，更是一个产品簇框架的铺垫。如果说我们关注项目的快速交付，不如说我们更关注 <strong>如何提供一个底层框架以支持不同项目的产品簇框架开发，从而最终支持项目的快速交付</strong> 。  </p>
<p>也只有这样，你辛苦付出的代码，你才会更加珍惜和不断为之冥想、维护和改进。<br />
也只有这样，你公司里面的其他项目才会更愿意和信赖使用，因为框架是你直接提供的。<br />
也只有这样，代码才得以永恒，因为这种思想在你、我、各个项目团队间不断传递，共存在每个开发人员的脑里和心中，而不是为某个自私的人把持着。  </p>
<h2>2.20.4 项目内涵</h2>
<p><strong>框架外延</strong> 是针对特定领域项目以外的问题。然而，在使用PhalApi进行项目开发时，我们如果只关注这些是无用的，我们还要关注项目的实际开发，如何编写代码，即：项目内涵。  </p>
<p>项目内涵是指完成特定领域功能所需要的前置条件、基础设施和工作流程。  </p>
<p>这里面着重讲解复杂的领域和更广义的数据源这两方面。不是说其他的方面不重要，而是这两方既重要但又常常为人们所误解误用。  </p>
<h3>(1)复杂的领域业务层</h3>
<p>在一个项目架构里面，有三个主要模型：设计模型、领域模型和代码模型。设计模型在选择PhalApi时已大体确定，领域模式则需要项目干系人员消化、理解并表达出来。对于开发人员，代码模型则是他们表达的媒介。  </p>
<p>这一层，主要关注的是领域业务规则的处理。所以，我们抛开外界客户端接口调用的签名验证、参数获取、安全性等问题，也不考虑数据从何而来、存放于何处，而是着重关注对领域业务数据的处理上。  </p>
<p>根据这么年来的工作、项目开发和学习，这里有一些建议。</p>
<h4>规则出现且出现一次</h4>
<p>领域之所以复杂，在于规则众多。如果不能很好地把控这些规则，当规则发生变化时，就会出现很大的问题。在开发过程中，要注意对规则进行提炼并且放置在一个指定的位置。如对游戏玩家的经验计算等级时，这样一个规则就要统一好。不要到处都有类型相同的计算接口。</p>
<h4>释意接口</h4>
<p>领域的逻辑是对现实业务场景的再解释。现实的因素充满变数并且由人为指定，所以不能简单的在计算机中“推导”出领域逻辑。在开发过程中，要特别对这些领域逻辑准确并很好的解释，以便后面接手的同学可以更容易理解和明白这些流程、限制和规则。  </p>
<p>其中一个有力的指导就是释意接口。对接口签名甚至是对变量命名的仔细推敲都是很有益处的，因为名字能正名份，不至于混淆或者含糊不清。</p>
<h4>代码保持在同一高度</h4>
<p>领域层关注的是流程、规则，所以当你进行用户个性化分流和排序时，不应该把底层网络接口请求的细节也放到这里流程里面。把底层技术实现的细节和业务规则的处理分开是很有好处的，这样便于更清晰领域逻辑的表达，也助于单元测试时的测试桩模拟。  </p>
<h3>(2)更广义的数据源层</h3>
<p>领域层固然重要，但如果没有数据源层，领域层就是一个空中楼阁。  </p>
<p>但不应把数据源就理所当然地对等成数据库。因为这种观念很常见但也很狭隘。首先，很多项目在对数据存储时，不一定会落地存储，即使落地也不一定使用数据库。我曾经在一家游戏公司任职时，就看到他们使用了文件来存放。相信，你也看到过。其次，在现在多客户端多系统的交互背景下，很多系统都需要进行数据共享和通信，为了提高服务器的性能也会使用到缓存。这些场景下，会导致数据是通过接口来获取，或者来源于缓存。可以看出，如果把数据源就看作是MySql，是非常局限的。  </p>
<p>我们在PhalApi中继续使用了Model层，因为受MVC模式的影响，大家都对Model层非常熟悉。但我们却为它赋予了新的诠释和活力。  </p>
<p>Model导获取的数据，可以是来自数据库的读取，也可以是通过开放平台接口获取的数据，也可以是不落地直接存放于缓存的数据。</p>
<h2>2.20.5 测试先行和真实的测试</h2>
<p>框架外延和项目内涵，分别是对 <strong>开发-配置-使用 模式</strong> 前半部分和后半部分的诠释。  </p>
<p>剩下的问题就在于，如何评判我们编写的代码是好的，是美的，并且能够按期望地工作？  </p>
<p>如果只是主观地判断，显然是不可靠的也不是完全可信的。例如“我觉得我这样没问题啊！”，即使代码能够正常运行，也不并代表在其他临界或者极端的情况也能如期运行，也不代表这些代码就具备了一些好的品质，更不代表这些代码遵循了我们的架构明显的编程风格：编写人容易理解的代码。  </p>
<p>既然主观不可靠，就应该转到客观上的标准。  </p>
<p>静态的代码分析，是很有用的，但对于最终需要动态执行的代码，还是需要单元测试才能更好的探知代码内部每个角落的状况。  </p>
<p>这里，我们强烈推荐测试先行，也就是测试驱动开发。可能你已对此耳熟能详，也可能你略知一二。  </p>
<p>但请相信，测试先行是值得的。关于真实的测试，即编写PHPUnit单元测试，关于PHPUnit的使用，请查看： <a href="https://phpunit.de/manual/3.7/zh_cn/automating-tests.html">PHPUnit Manual &#8211; 第 1 章 自动化测试</a> 。  </p>
<h2>2.20.6 PhalApi架构的设计意图</h2>
<h3>(1)设计意图</h3>
<p>PhalApi是开放式的框架，这里不仅仅体现在源代码开放，产品开放，还表现在思想开放。  </p>
<p>这样说，可能有些抽象。若落实到代码层次，你会发现PhalApi提供了接口上的约束，并为每块接口提供了很好的扩展机制。也就是说，一旦你发现已提供的功能不满足你项目的需要，你可以轻松定制、扩展和升级。  </p>
<p>这就是我们的设计意图。  </p>
<p>对于新手，你可以快速使用这个框架；对于老手，你则可以定制它。  </p>
<h3>(2)协同合作和关注点</h3>
<p><strong>开发-配置-使用 模式</strong> 并不是为了传递 “君君臣臣、父父子子”这样的封建等级观念。而是为了资源更好的调配，最终更好地完成项目的开发。  </p>
<p>架构系统有分层的思想，这里也一样。通过高级开发、开发工程师和新手各尽其才、各施其职，能够更好的提高各自的关注点，并发挥他们应有的能力和价值。  </p>
<p>比如对于高级开发，我们希望他们能够抽离业务，并且产出一些公司内可以使用的通用组件、核心技术甚至是产品簇开发框架。而对于他们来说，也许这也正是他们所喜爱的，因为他们在攻关一些技术难点，又或者在解决一些他们觉得有挑战的工作。  </p>
<p>对于项目负责人，也就是开发工程师，他们更关注的是整个项目的运行和所能提供的功能。而这一些功能需要各业务规则在代码上的体现，最终又会直接或间接落到某些基础设施的支持上，一如数据库的查询操作。显然，他们不希望每次都为这样通用的技术支持重复开发。如果有已经能够直接拿来使用的代码，那该多好。网上虽然资源众多，但符合公司项目使用的，少之甚少，又或许会有这样那样的限制。如果有公司内部可重用的组件库，这种情况会大为改观。  </p>
<p>对于新手，我们要求明显会低很多。简单来说，他们会使用就可以了。当然，我们也推荐新手在熟练使用的情况下，再深化到底层，慢慢过渡到大工。  </p>
<h3>(3)这只是一个开始</h3>
<p>尽管 我们提供了这样架构明显的编程风格，但仍然需要你以及你的团队来遵循。  </p>
<p>正如前面所说的，项目的每一行代码和每一个命名都来自你的思考和双手的输入。也正如此，你的付出让你更深刻体会到编程的乐趣，特别是项目发挥和实现了有价值的业务功能时。这时，你会发现维护别人的代码不再是一件痛苦的事件，与团队的合作也变得更加融洽，因为和你一起奋斗努力的是一支精英团队。  </p>
<p>请记住，这只是一个开始，一个起点。</p>
<hr />
<h4><a href="/wikis/%5B2.19%5D-%E6%89%A9%E5%B1%95%E5%BA%93%EF%BC%9A%E5%8F%AF%E9%87%8D%E7%94%A8%E7%9A%84%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.21%5D-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%9F%E4%B8%80%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E7%AE%80%E6%98%8E%E7%89%88%EF%BC%9ACentOs---Nginx---php-fpm---MySql-%5B--Memcached%5D.html">下一章</a></h4>
<h1>[2.21]-服务器统一部署方案简明版：CentOs---Nginx---php-fpm---MySql-[--Memcached]</h1>
<h4><a href="/wikis/%5B2.20%5D-%E7%BA%A6%E5%AE%9A%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%9E%B6%E6%9E%84%E6%98%8E%E6%98%BE%E7%9A%84%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.22%5D-%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%B2%BE%E7%9B%8A%E9%A1%B9%E7%9B%AE%E5%92%8C%E5%9B%A2%E9%98%9F%E5%BB%BA%E8%AE%BE.html">下一章</a></h4>
<hr />
<h2>2.21.1 前言</h2>
<p>目前服务器架构通常采用：CentOs + Nginx + php-fpm + MySql [+ Memcached]，现将部署方案安装方法、遇到的问题和解决方法、启动方式总结如下，以便后期服务器快速部署。  </p>
<pre><code>以下文档的是经过几个深夜整理并在阿里云服务器真实部署通过，运行良好，整个过程熟悉后约共半小时，可以进一步考虑做成自动化部署脚本。
如是，请把脚本也邮件发我一份。^_^</code></pre>
<h2>2.21.2 统一约定</h2>
<ul>
<li>全部使用编译安装</li>
<li>安装路径路径：/usr/local/</li>
<li>
<p>安装包存放位置：/home/apps/web/</p>
<p><strong>特别注意：请尽量不要使用root账号操作服务器，特别是对外服务器，谢谢。</strong>  </p>
</li>
</ul>
<h2>2.21.3 依赖库和开发工具</h2>
<pre><code class="language-javascript">yum -y install gcc gcc-c++ autoconf libjpeg libjpeg-devel libpng libpng-devel freetype freetype-devel libxml2 libxml2-devel zlib zlib-devel glibc glibc-devel glib2 glib2-devel bzip2 bzip2-devel ncurses ncurses-devel curl curl-devel e2fsprogs e2fsprogs-devel krb5 krb5-devel libidn libidn-devel openssl openssl-devel openldap openldap-devel nss_ldap openldap-clients openldap-servers

# nginx
yum -y install pcre-devel zlib-devel

# php
yum -y install gd-devel libjpeg-devel libpng-devel freetype-devel libxml2-devel curl-devel freetype-devel

# mysql
yum -y install bison gcc gcc-c++ autoconf automake zlib* libxml* ncurses-devel libtool-ltdl-devel* mysql-devel

# sz rz
yum install lrzsz</code></pre>
<h2>2.21.4 安装Nginx 1.6.0</h2>
<h3>参考官方网站</h3>
<pre><code>http://wiki.nginx.org/Install</code></pre>
<h3>(1)安装</h3>
<pre><code class="language-javascript">./configure --prefix=/usr/local/nginx
make
make install</code></pre>
<h3>(2)启动</h3>
<pre><code class="language-javascript">/usr/local/nginx/sbin/nginx</code></pre>
<h3>(3)重启</h3>
<pre><code class="language-javascript">/usr/local/nginx/sbin/nginx -t
/usr/local/nginx/sbin/nginx -s reload</code></pre>
<h3>(4)更多配置</h3>
<pre><code class="language-javascript">server {
    listen 80;
    server_name test.wetime.com;
    root /home/apps/projects/test.wetime.com;
    charset utf-8;
    access_log logs/test.wetime.com.access.log;
    error_log logs/test.wetime.com.error.log;
    location / {
        index index.html index.htm index.php;
    }
    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}</code></pre>
<h2>2.21.5 安装PHP-FPM 5.3.5</h2>
<h3>(1)参考</h3>
<pre><code>http://blog.csdn.net/dc_726/article/details/9519619</code></pre>
<h3>(2)安装</h3>
<pre><code class="language-javascript">./configure --prefix=/usr/local/php --with-mcrypt --with-zlib --with-curl --with-gd --disable-debug --enable-fpm --enable-mbstring --enable-sockets --enable-zip --enable-xml --enable-pdo</code></pre>
<h4>温馨提示</h4>
<p>如果提示以下错误：</p>
<pre><code class="language-javascript">If configure fails try --with-jpeg-dir=&lt;DIR&gt;
configure: error: libpng.(a|so) not found.</code></pre>
<p>则需要执行以下命令：</p>
<pre><code class="language-javascript">cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so

yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel</code></pre>
<h3>(3)启动</h3>
<pre><code class="language-javascript">/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini</code></pre>
<h3>(4)重启</h3>
<pre><code class="language-javascript">/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini -t 
killall php
/usr/local/php/sbin/php-fpm -y=/usr/local/php/etc/php-fpm.conf -c=/usr/local/php/etc/php.ini</code></pre>
<h3>(5)错误处理与依赖</h3>
<pre><code class="language-javascript">cp  -frp /usr/lib64/libjpeg.* /usr/lib/
ln -s /usr/lib64/libpng.so /usr/lib/libpng.so
yum install libmcrypt libmcrypt-devel mcrypt mhash
yum install unixODBC-devel

我们可以在命令下输入： 

yum localinstall –nogpgcheck http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/free/el/updates/6/x86_64/rpmfusion-free-release-6-1.noarch.rpm
yum localinstall –nogpgcheck http://download1.rpmfusion.org/nonfree/el/updates/6/x86_64/rpmfusion-nonfree-release-6-1.noarch.rpm

安装这3个东东后，我们就可以直接使用yum install libmcrypt来安装啦～</code></pre>
<h3>(6)php扩展安装</h3>
<pre><code class="language-javascript">#安装mysql扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysql=/usr/local/mysql/
make
make install

#安装mysqli扩展
 ./configure --with-php-config=/usr/local/php/bin/php-config --with-mysqli=/usr/local/mysql/bin/mysql_config
make
make install</code></pre>
<p><em>扩展包位置在./php-5.3.5/ext/mysql，需要使用到 /usr/local/php/bin/phpize，扩展so应该放到：/usr/lib/php/modules/</em>  </p>
<p>此外，</p>
<pre><code>#PHP之pdo_mysql扩展安装步骤

# http://www.smallbigcake.com/2012/08/05/1572/ 
# http://pecl.php.net/get/PDO_MYSQL-1.0.2.tgz 

./configure --with-php-config=/usr/local/php/bin/php-config --with-pdo-mysql=/usr/local/mysql 

如果提示“libmysqlclient.so.18: cannot open shared object file: No such file or directory in ”错误则参考 ：http://blog.itpub.net/26230597/viewspace-1161322/， 
或进行软链： 
ln -s /usr/local/mysql/lib/libmysqlclient.so.18 /usr/lib64/libmysqlclient.so.18 </code></pre>
<h2>2.21.6 安装MYSQL 5.6.10</h2>
<h3>(1)参考</h3>
<pre><code>http://www.cnblogs.com/xiongpq/p/3384681.html</code></pre>
<h3>(2)mysql-5.6.10.tar.gz下载</h3>
<pre><code>http://pan.baidu.com/share/link?shareid=3729862999&amp;uk=1327790029</code></pre>
<h3>(3)前期清理和依赖</h3>
<pre><code class="language-javascript">rpm -e mysql   //普通删除模式
rpm -e --nodeps mysql    // 强力删除模式，如果使用上面命令删除时，提示有依赖的其它文件，则用该命令可以对其进行强力删除
yum -y install make gcc-c++ cmake bison-devel  ncurses-devel</code></pre>
<h3>(4)安装</h3>
<pre><code class="language-javascript">cmake \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/usr/local/mysql/data \
-DSYSCONFDIR=/etc \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MEMORY_STORAGE_ENGINE=1 \
-DWITH_READLINE=1 \
-DMYSQL_UNIX_ADDR=/var/lib/mysql/mysql.sock \
-DMYSQL_TCP_PORT=3306 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DEXTRA_CHARSETS=all \
-DDEFAULT_CHARSET=utf8 \
-DDEFAULT_COLLATION=utf8_general_ci

make &amp;&amp; make install</code></pre>
<h3>(5)ERROR! The server quit without updating PID file</h3>
<pre><code class="language-javascript">#去掉干扰的配置
mv /etc/my.cnf /etc/my.cnf.bak

#添加日记纪录
vim /usr/local/mysql/my.cnf
log-error=/var/log/mysqld.log
innodb_force_recovery=1</code></pre>
<h2>2.21.7 [可选]MEMCACHE 1.4.20</h2>
<h3>(1)参考官网</h3>
<pre><code>http://www.memcached.org/downloads</code></pre>
<h3>(2)更多参考</h3>
<pre><code>http://crtlib.blog.163.com/blog/static/133158013201392661912837/</code></pre>
<h3>(3)先安装依赖包</h3>
<pre><code class="language-javascript">./configure --prefix=/usr/local/libevent/
make
make install</code></pre>
<h3>(4)安装</h3>
<pre><code class="language-javascript">wget http://memcached.org/latest tar -zxvf memcached-1.x.x.tar.gz cd memcached-1.x.x ./configure &amp;&amp; make &amp;&amp; make test &amp;&amp; sudo make install</code></pre>
<h3>(5)最后成功安装后启动</h3>
<pre><code class="language-javascript">/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 128 -P /tmp/memcached.pid</code></pre>
<h3>(6)memcached 扩展</h3>
<pre><code class="language-javascript">#http://blog.sina.com.cn/s/blog_66ac09390101drxc.html
#http://www.linuxidc.com/Linux/2013-07/87743.htm

#准备依赖包
./configure --prefix=/usr/local/libmemcached --with-memcached
make 
make install

#安装 扩展
/usr/local/php/bin/pecl install memcached
接着输入
libmemcached directory [no] : /usr/local/libmemcached</code></pre>
<hr />
<h4><a href="/wikis/%5B2.20%5D-%E7%BA%A6%E5%AE%9A%E7%BC%96%E7%A8%8B%EF%BC%9A%E6%9E%B6%E6%9E%84%E6%98%8E%E6%98%BE%E7%9A%84%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B2.22%5D-%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%B2%BE%E7%9B%8A%E9%A1%B9%E7%9B%AE%E5%92%8C%E5%9B%A2%E9%98%9F%E5%BB%BA%E8%AE%BE.html">下一章</a>#[2.22]-更多工具：精益项目和团队建设</h4>
<h4><a href="/wikis/%5B2.21%5D-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%9F%E4%B8%80%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E7%AE%80%E6%98%8E%E7%89%88%EF%BC%9ACentOs---Nginx---php-fpm---MySql-%5B--Memcached%5D.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91.html">下一章</a></h4>
<hr />
<h2>2.22.1 PHPUnit：自动化单元测试</h2>
<p>目前核心框架单元测试的覆盖率（高达90%以上！！！此处应该有掌声~）：
<img src="http://webtools.qiniudn.com/131634_NHlk_256338.jpg" alt="a pic" />  </p>
<p>注意，此测试截图并不是最新的，但我们一直都在致力坚持单元测试。<br />
这一点，你可以在单元测试的代码中找到证明。  </p>
<h2>2.22.2 Phing：一键部署、快速发布</h2>
<p>无论如何，都应该走自动化发布流程，避免人工地打包、上传、解压、改生产配置这些重复性的人工操作。  </p>
<p>在自动化发布中，Phing是个不错的尝试。  </p>
<p>以下是我某个项目中的使用配置，出于对项目的保护，部分数据已删除，但仍然可以参考。</p>
<h3>(1)发布配置 - build.xml</h3>
<p>主要的操作有：</p>
<ul>
<li>
<ol>
<li>备份当前代码，并删除一天前的备份</li>
</ol>
</li>
<li>
<ol>
<li>从SVN签出最新的发布代码</li>
</ol>
</li>
<li>
<ol>
<li>执行配置检测脚本</li>
</ol>
</li>
<li>
<ol>
<li>代码发布切换，并替换使用线上配置文件</li>
</ol>
</li>
<li>
<ol>
<li>移除单元测试初始化文件，以免误执行</li>
</ol>
</li>
</ul>
<pre><code class="language-javascript">&lt;?xml version="1.0" encoding="UTF-8"?&gt;

&lt;!-- ============================================  --&gt;
&lt;!-- PhalApi                       --&gt;
&lt;!-- @dogstar 20141221                             --&gt;
&lt;!-- ============================================  --&gt;

&lt;project name="demo.phalapi.com" default="build"&gt;

    &lt;property 
        name="backup_path" 
        value="/home/apps/backup/demo.phalapi.com" 
        override="true" /&gt;
    &lt;property 
        name="backup_prefix" 
        value="demo.phalapi.com_phing_backup_" 
        override="true" /&gt;
    &lt;property 
        name="svn_todir" 
        value="/home/apps/svn/demo.phalapi.com" 
        override="true" /&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: prepare                               --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="prepare"&gt;
        &lt;mkdir dir="./Runtime" /&gt;
        &lt;mkdir dir="${svn_todir}" /&gt;
        &lt;mkdir dir="${backup_path}" /&gt;
    &lt;/target&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: svn checkout                          --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="svncheckout"&gt;
        &lt;svncheckout 
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release" 
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" /&gt;
    &lt;/target&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: svn update                            --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="svnup" depends="svncheckout"&gt;
        &lt;svnupdate 
            repositoryurl="svn://127.0.0.1/PhalApi/demo/release" 
            username="test"
            password="123456"
            nocache="true"
            todir="${svn_todir}" /&gt;
    &lt;/target&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: check config                          --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="checkconfig"&gt;
    &lt;php expression="include('${svn_todir}/Tools/check_config.php')" /&gt;
        &lt;php 
            function="checkProd" 
            class="Tools_Check_Config" 
            returnProperty="checkProdErrorMsg" /&gt;

        &lt;fail msg="${checkProdErrorMsg}" if="checkProdErrorMsg" /&gt;
    &lt;/target&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: backup                                --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="backup"&gt;
        &lt;php 
            expression="date('Ymd', strtotime('-1 day'))" 
            returnProperty="backup_version_yesterday" /&gt;
        &lt;php 
            expression="date('Ymd')" 
            returnProperty="backup_version_today" /&gt;
        &lt;delete&gt;
            &lt;fileset dir="${backup_path}"&gt;
                &lt;exclude name="${backup_prefix}${backup_version_yesterday}*" /&gt;
                &lt;exclude name="${backup_prefix}${backup_version_today}*" /&gt;
            &lt;/fileset&gt;
        &lt;/delete&gt;

        &lt;php 
            expression="date('YmdHis')" 
            returnProperty="backup_version" /&gt;
        &lt;zip destfile="${backup_path}/${backup_prefix}${backup_version}.zip" basedir="." &gt;
            &lt;fileset dir="."&gt;
                &lt;include name="**/**" /&gt;

                &lt;exclude name="./Runtime" /&gt;
                &lt;exclude name="./Runtime/**" /&gt;
                &lt;exclude name="./.svn" /&gt;
                &lt;exclude name="./.svn/**" /&gt;
            &lt;/fileset&gt;
        &lt;/zip&gt;
        &lt;copy 
            file="${backup_path}/${backup_prefix}${backup_version}.zip" 
            tofile="${backup_path}/${backup_prefix}lastest.zip" 
            overwrite="true" /&gt;
    &lt;/target&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: build                                 --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="build" depends="prepare,svnup,checkconfig,backup"&gt;
        &lt;copy todir="." overwrite="true" &gt;
            &lt;fileset dir="${svn_todir}"&gt;
                &lt;include name="**/**" /&gt;

                &lt;exclude name="${svn_todir}/Config/dbs.php" /&gt;
                &lt;exclude name="${svn_todir}/Config/sys.php" /&gt;
                &lt;exclude name="${svn_todir}/Test" /&gt;
                &lt;exclude name="${svn_todir}/Test/**" /&gt;
            &lt;/fileset&gt;
        &lt;/copy&gt;

        &lt;copy 
            file="./Config/dbs.php.prod" 
            tofile="./Config/dbs.php" 
            overwrite="true" /&gt;
        &lt;copy 
            file="./Config/sys.php.prod" 
            tofile="./Config/sys.php" 
            overwrite="true" /&gt;

        &lt;!-- 避免在生产环境执行PHPUnit，故把测试环境的初始文件移开 --&gt;
        &lt;move 
            file="./Test/test_env.php"
            tofile="./Test/test_env.php.bak" 
            overwrite="true"/&gt;
    &lt;/target&gt;

&lt;/project&gt;
</code></pre>
<h3>(2)回滚配置 - rollback.xml</h3>
<p>主要操作：  </p>
<ul>
<li>
<ol>
<li>回滚到上一个版本</li>
</ol>
</li>
</ul>
<pre><code class="language-javascript">&lt;?xml version="1.0" encoding="UTF-8"?&gt;

&lt;!-- ============================================  --&gt;
&lt;!-- PhalApi                       --&gt;
&lt;!-- @dogstar 20141222 冬至                        --&gt;
&lt;!-- ============================================  --&gt;

&lt;project name="demo.phalapi.com" default="rollback"&gt;

    &lt;property 
        name="backup_path" 
        value="/home/apps/backup/demo.phalapi.com" 
        override="true" /&gt;
    &lt;property 
        name="backup_prefix" 
        value="demo.phalapi.com_phing_backup_" 
        override="true" /&gt;
    &lt;property 
        name="svn_todir" 
        value="./__svn__" 
        override="true" /&gt;

    &lt;!-- ============================================  --&gt;
    &lt;!-- Target: rollback                              --&gt;
    &lt;!-- ============================================  --&gt;
    &lt;target name="rollback" &gt;
        &lt;unzip file="${backup_path}/${backup_prefix}lastest.zip" todir="." &gt;
            &lt;fileset dir="."&gt;
                &lt;include name="*.zip"/&gt;
            &lt;/fileset&gt;
        &lt;/unzip&gt;
    &lt;/target&gt;

&lt;/project&gt;
</code></pre>
<h2>2.22.3 autobench：接口压力测试与可视化图表</h2>
<p>可以通过以下的脚本来进行，使用示例：  </p>
<pre><code class="language-javascript">$ ./autobench.sh
Usage: ./autobench.sh &lt;host&gt; &lt;uri&gt;

 - ./autobench.sh www.baidu.com /index.php</code></pre>
<p>参数可以自行调整。  </p>
<h3>(1)利用bench2graph生成可视化图表</h3>
<h4>纯PHP访问 - 入口欢迎接口</h4>
<p><img src="http://webtools.qiniudn.com/1421422942BjP7K5M5.jpg" alt="a pic" /></p>
<h4>mysql访问 - 事件获取接口</h4>
<p><img src="http://webtools.qiniudn.com/14214229738ftM0Nck.jpg" alt="a pic" /></p>
<h4>带MC缓存的访问 - 应用入口 - 带缓存的身份token验证</h4>
<p><img src="http://webtools.qiniudn.com/1421424027OYV4Ae5i.jpg" alt="a pic" /></p>
<h3>(2)详细的数据报表</h3>
<pre><code class="language-javascript">dem_req_rate    req_rate_demo.phalapi.com     con_rate_demo.phalapi.com     min_rep_rate_demo.phalapi.com avg_rep_rate_demo.phalapi.com    max_rep_rate_demo.phalapi.com stddev_rep_rate_demo.phalapi.com      resp_time_demo.phalapi.com       net_io_demo.phalapi.com       errors_demo.phalapi.com
5       5.0     5.0     4.8     5.0     5.2     0.1     22.4    2.9     0
25      25.0    25.0    25.0    25.0    25.0    0.0     21.6    14.3    0
45      45.0    45.0    45.0    45.0    45.0    0.0     22.3    25.7    0
65      64.9    64.9    64.7    64.7    64.7    0.0     25.4    37.0    0
85      84.8    84.8    84.6    84.6    84.6    0.0     28.6    48.3    0
105     104.6   104.6   0.0     0.0     0.0     0.0     34.4    59.7    0
125     124.0   124.0   0.0     0.0     0.0     0.0     42.2    70.7    0
145     143.8   143.8   0.0     0.0     0.0     0.0     59.2    82.0    0
165     147.4   147.4   0.0     0.0     0.0     0.0     262.9   84.1    0
185     151.1   151.1   0.0     0.0     0.0     0.0     429.7   86.2    0</code></pre>
<h3>(3)附脚本</h3>
<pre><code class="language-javascript">#!/bin/bash

if [ $# -eq 0 ]; then
    echo "Usage: $0 &lt;host&gt; &lt;uri&gt;"
    echo ""
    echo " - $0 www.baidu.com /index.php"
    echo ""
    exit
fi

DM=$1
URL=$2

#--signle_host 只测单机
#--host1 测试主机地址
#--uri1 host1 测试URI
#--quiet 安静模式
#--low_rate 测试时最低请求数(指 httperf)
#--hight_rate 测试时最高请求数
#--rate_step 每次测试请求数增加步长
#--num-call 每连接中发起联接数，一般是1
#--num_conn 测试联接数
#--file 测试结果输出的 tsv文件

autobench \
--single_host \
--host1=$DM \
--port1=80 \
--uri1=$URL \
--low_rate=5 \
--high_rate=200 \
--rate_step=20 \
--num_call=1 \
--num_conn=500 \
--timeout=10 \
--file ./$DM.tsv</code></pre>
<h2>2.22.4 xhprof：性能分析工具</h2>
<p>xhprof是一个不错的内部性能分析工具，这里不过多的讲述此工具的特点和使用，但会以对PhalApi进行的一个性能测试展示它的分析效果。  </p>
<h3>(1)测试的接口服务</h3>
<p><a href="http://api.phalapi.com">http://api.phalapi.com</a> /demo/?service=Default.Index&amp;username=test</p>
<h3>(2)Overall Summary</h3>
<pre><code class="language-javascript">Total Incl. Wall Time (microsec):     7,873 microsecs
Total Incl. CPU (microsecs):    7,999 microsecs
Total Incl. MemUse (bytes):    304,456 bytes
Total Incl. PeakMemUse (bytes):    306,616 bytes
Number of Function Calls:        338</code></pre>
<h3>(3)Top 10耗时</h3>
<table>
<thead>
<tr>
<th>Function Name</th>
<th>Calls</th>
<th>Calls%</th>
<th>Incl. Wall Time(microsec)</th>
<th>IWall%(microsec)</th>
<th>Excl. Wall Time</th>
<th>EWall%</th>
</tr>
</thead>
<tbody>
<tr>
<td>PhalApi_Loader::loadClass</td>
<td>10</td>
<td>3.00%</td>
<td>3,020</td>
<td>38.40%</td>
<td>1,038</td>
<td>13.20%</td>
</tr>
<tr>
<td>PhalApi_Loader::loadClass1</td>
<td>4</td>
<td>1.20%</td>
<td>785</td>
<td>10.00%</td>
<td>398</td>
<td>5.10%</td>
</tr>
<tr>
<td>run_init::Public/init.php</td>
<td>1</td>
<td>0.30%</td>
<td>3,915</td>
<td>49.70%</td>
<td>327</td>
<td>4.20%</td>
</tr>
<tr>
<td>file_exists</td>
<td>18</td>
<td>5.30%</td>
<td>249</td>
<td>3.20%</td>
<td>249</td>
<td>3.20%</td>
</tr>
<tr>
<td>main()</td>
<td>1</td>
<td>0.30%</td>
<td>7,873</td>
<td>100.00%</td>
<td>248</td>
<td>3.20%</td>
</tr>
<tr>
<td>load::zh_cn/common.php</td>
<td>2</td>
<td>0.60%</td>
<td>237</td>
<td>3.00%</td>
<td>237</td>
<td>3.00%</td>
</tr>
<tr>
<td>PhalApi_Loader::load</td>
<td>9</td>
<td>2.70%</td>
<td>3,380</td>
<td>42.90%</td>
<td>226</td>
<td>2.90%</td>
</tr>
<tr>
<td>PhalApi_DI::get</td>
<td>9</td>
<td>2.70%</td>
<td>1,268</td>
<td>16.10%</td>
<td>223</td>
<td>2.80%</td>
</tr>
<tr>
<td>PhalApi_Translator::addMessage</td>
<td>2</td>
<td>0.60%</td>
<td>522</td>
<td>6.60%</td>
<td>136</td>
<td>1.70%</td>
</tr>
<tr>
<td>DI</td>
<td>15</td>
<td>4.40%</td>
<td>645</td>
<td>8.20%</td>
<td>134</td>
<td>1.70%</td>
</tr>
<tr>
<td>class_exists</td>
<td>11</td>
<td>3.30%</td>
<td>1,383</td>
<td>17.60%</td>
<td>130</td>
<td>1.70%</td>
</tr>
<tr>
<td>load::Config/sys.php</td>
<td>1</td>
<td>0.30%</td>
<td>120</td>
<td>1.50%</td>
<td>120</td>
<td>1.50%</td>
</tr>
</tbody>
</table>
<p>对应的图表如下：<br />
<img src="http://webtools.qiniudn.com/phalapi_xhprxf.jpg" alt="a pic" /><br />
从中可以看出，主要的耗时途径在于文件的加载，下面将进一步探讨。</p>
<h3>(4)Top 1耗时深入</h3>
<p><img src="http://webtools.qiniudn.com/top1_sheet-small.png" alt="a pic" />    </p>
<p>所加载的文件如下：<br />
<img src="http://webtools.qiniudn.com/top1_loader.png" alt="a pic" />  </p>
<h2>2.22.5 Jenkis和Sonar：持续集成和静态代码分析</h2>
<p>PhalApi从来不会隐藏自己的设计，当然，我们也不会隐藏我们内部的各个细节以及存在的问题。<br />
但和其他隐藏了技术债务的框架相比，PhalApi敢于展示自己的静态代码分析报告，并且它也是做得相当出色的。  </p>
<p>以下截图来自 GIT@OSC 上的静态代码分析：
<img src="http://webtools.qiniudn.com/sonar_code_analysis.png" alt="a pic" />  </p>
<p>当你的项目很重要时，也可以使用Jenkis或者Sonar进行静态代码的分析。<br />
不要个人主观地觉得你的代码风格写得好，而是交由专业的分析工具进行解剖，并理解各个数据报表背后的含义，然后改进之。</p>
<h2>2.22.6 Git/SVN：更多的版本控制</h2>
<h2>2.22.7 WIKI：团队沟通与文档交流</h2>
<h3>(1)markdown接口模板</h3>
<pre><code>#3.2 接口文档模板
##1、功能说明
_请在这里放置简短的接口功能说明。_  

##2、接口URL
/?service= _接口服务名称_  + 公共参数(是否免登录态？)

##3、接口参数 [跳转](http://demo.phalapi.com/demo/checkApiParams.php?service=) 

参数|必须|默认值|说明
---|---|---|---
user_id|1||用户id

##4、返回结果

###返回字段
参数|类型|说明
---|---|---
data.username|string|用户名

###结果示例
{
    "ret": 200,
    "data": {
       ....  //更多结果的说明

        "msg": ""
    },
    "msg": ""
}

###请求示例
_请放置一个接口请求的链接。_

http://demo.phalapi.com/demo/?service=</code></pre>
<h2>2.22.8 静态代码分析工具 - phpmetrics</h2>
<p>以下是针对本框架核心代码所做的分析报告：  </p>
<p><img src="http://7qnay5.com1.z0.glb.clouddn.com/QQ截图20150807230905.jpg" alt="a pic" /><br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/QQ截图20150807230920.jpg" alt="a pic" />   </p>
<p>PS：从报告的评估可以看出，我们的框架明显具有相当 高的可维护性。  </p>
<hr />
<h4><a href="/wikis/%5B2.21%5D-%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BB%9F%E4%B8%80%E9%83%A8%E7%BD%B2%E6%96%B9%E6%A1%88%E7%AE%80%E6%98%8E%E7%89%88%EF%BC%9ACentOs---Nginx---php-fpm---MySql-%5B--Memcached%5D.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91.html">下一章</a>#[3.1]-扩展类库：微信开发</h4>
<h4><a href="/wikis/%5B2.22%5D-%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%B2%BE%E7%9B%8A%E9%A1%B9%E7%9B%AE%E5%92%8C%E5%9B%A2%E9%98%9F%E5%BB%BA%E8%AE%BE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81.html">下一章</a></h4>
<hr />
<h2>3.1.1 扩展类库：微信开发</h2>
<p>此扩展可用于微信的服务号、订阅号、企业号等功能开发，在PhalApi框架下简单配置即可开发使用。</p>
<h2>3.1.2 安装和配置</h2>
<h3>(1)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>Wechat</strong> 微信包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>Wechat</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/Wechat/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！接下是插件的配置。</p>
<h3>(2)扩展包配置</h3>
<p>为了让微信扩展包能接收来自微信服务器的信息以及返回信息给用户，我们需要在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">
    /**
     * 插件注册 - 项目级重用
     */
  'Wechat' =&gt; array(
    'plugins' =&gt; array(
        Wechat_InMessage::MSG_TYPE_TEXT =&gt; array('Plugin_Menu',),
        Wechat_InMessage::MSG_TYPE_IMAGE =&gt; array(),
        Wechat_InMessage::MSG_TYPE_VOICE =&gt; array(),
        Wechat_InMessage::MSG_TYPE_VIDEO =&gt; array(),
        Wechat_InMessage::MSG_TYPE_LOCATION =&gt; array(),
        Wechat_InMessage::MSG_TYPE_LINK =&gt; array(),
        Wechat_InMessage::MSG_TYPE_EVENT =&gt; array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_EVENT =&gt; array(),
        Wechat_InMessage::MSG_TYPE_DEVICE_TEXT =&gt; array(),
    ),
  )
</code></pre>
<p>简单说明一下上面配置的作用，很明显，Key为对应的消息类型，如：文本、位置、语音、图片等；然后是各种消息类型对应的处理的类名，可以有多个，从上到下依次处理。<br />
如果觉得配置很多，可以只配置需要用到的消息类型。另外，如果你的配置是其他途径存放的，也亦然。  </p>
<h2>3.1.3 入门使用</h2>
<h3>(1)微信入口文件</h3>
<p>可以按将下面的入口代码得到你的项目下：</p>
<pre><code class="language-javascript">//$ vim ./Public/weixin/index.php 

&lt;?php

// echo $_GET['echostr'];
// die();

if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
    die('Access denied!');
}

require_once dirname(__FILE__) . '/../init.php';

//装载项目代码和扩展类库
DI()-&gt;loader-&gt;addDirs(array('Demo', 'Library'));

/** ---------------- 微信轻聊版 ---------------- **/

$robot = new Wechat_Lite('YourTokenHere...', true);
$rs = $robot-&gt;response();
$rs-&gt;output();</code></pre>
<p><strong>特别地</strong> ，当首次接入微信时，需要将开头的两句注释去掉，以便通过微信的验证，即：</p>
<pre><code class="language-javascript">// echo $_GET['echostr'];
// die();</code></pre>
<h2>3.1.4 示例：让红包飞</h2>
<h3>(1)业务场景</h3>
<p>现在，快要过年了，各大企业都在派红包，这里，我们也模拟一下微信服务号上红包的派发。   </p>
<p>通常地，当我们需要添加一个新的微信服务号的功能时，可以两步走： <strong>先开发插件，再注册插件</strong> 。</p>
<h3>(2)开发插件</h3>
<p>很简单地：</p>
<pre><code class="language-javascript">// $vim ./Demo/Plugin/Money.php 

&lt;?php

class Plugin_Money implements Wechat_Plugin_Text {

    public function handleText($inMessage, &amp;$outMessage) {
        $outMessage = new Wechat_OutMessage_News();

        $item = new Wechat_OutMessage_News_Item();
        $item-&gt;setTitle('让红包飞~')
            -&gt;setDescription(sprintf('您已领取到一个%d元红包~', rand(1, 100)))
            -&gt;setPicUrl('http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg')
            -&gt;setUrl('http://git.oschina.net/dogstar/PhalApi-Library');

        $outMessage-&gt;addItem($item);
    }
}</code></pre>
<h3>(3)注册插件</h3>
<p>开发好插件后，需要在配置添加对哪种消息开启使用该新的插件。在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">        // 前面配置略 ...

        // 在前面追加红包插件，处理顺序由左到右
        Wechat_InMessage::MSG_TYPE_TEXT =&gt; array('Plugin_Money', 'Plugin_Menu', ), 
</code></pre>
<h3>(4)运行效果</h3>
<p><img src="http://webtools.qiniudn.com/20150411005257_976929b37873ae51ca54d5cb24a61ff0" alt="webwxgetmsgimg" />  </p>
<h2>3.1.5 微信开发调试</h2>
<p>为了便于进行微信开发的调试，我们专门提供了一个快速模拟微信请求的脚本。以下是对上面发红包的模拟请求：</p>
<pre><code class="language-javascript">$php ./Library/Wechat/Tests/send_wechat_text.php http://dev.wechat.com/weixin/ demo

&lt;xml&gt;
&lt;ToUserName&gt;&lt;![CDATA[oWNXvjipYqRViMpO8GZwXxE43pUY]]&gt;&lt;/ToUserName&gt;
&lt;FromUserName&gt;&lt;![CDATA[gh_43235ff1360f]]&gt;&lt;/FromUserName&gt;
&lt;CreateTime&gt;1423821388&lt;/CreateTime&gt;
&lt;MsgType&gt;&lt;![CDATA[news]]&gt;&lt;/MsgType&gt;
&lt;ArticleCount&gt;1&lt;/ArticleCount&gt;
&lt;Articles&gt;
&lt;item&gt;
&lt;Title&gt;&lt;![CDATA[让红包飞~]]&gt;&lt;/Title&gt;
&lt;Description&gt;&lt;![CDATA[您已领取到一个85元红包~]]&gt;&lt;/Description&gt;
&lt;PicUrl&gt;&lt;![CDATA[http://webtools.qiniudn.com/172906_61c8663a_121026.jpeg]]&gt;&lt;/PicUrl&gt;
&lt;Url&gt;&lt;![CDATA[http://git.oschina.net/dogstar/PhalApi-Library]]&gt;&lt;/Url&gt;
&lt;/item&gt;
&lt;/Articles&gt;
&lt;FuncFlag&gt;0&lt;/FuncFlag&gt;
&lt;/xml&gt;</code></pre>
<hr />
<h4><a href="/wikis/%5B2.22%5D-%E6%9B%B4%E5%A4%9A%E5%B7%A5%E5%85%B7%EF%BC%9A%E7%B2%BE%E7%9B%8A%E9%A1%B9%E7%9B%AE%E5%92%8C%E5%9B%A2%E9%98%9F%E5%BB%BA%E8%AE%BE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81.html">下一章</a>#[3.2]-扩展类库：代理模式下phprpc协议的轻松支持</h4>
<h4><a href="/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81.html">下一章</a></h4>
<hr />
<h2>3.2.1 扩展类库：代理模式下phprpc协议的轻松支持</h2>
<p>我们一直都建议在我们的项目中使用设计模式以便让代码更优雅，但我们更推荐只在合适的场景才使用合适的设计模式，而不是为了“显学”而生硬套用。设计模式又需要更高层的设计原则和工程思想作指导，才能更正确被使用发挥其作用。  </p>
<p>在PhalApi，正如前面所说的，我们引入并应用了很多设计原则，一如单一职责原则SPR、开放-封闭原则OCP等。因此，在这里我们可以在phprpc的基础上，利用代理模式优雅地扩展实现phpcpr协议。  </p>
<p>在需要使用phprpc协议进行接口调用的项目中，我们可以快速利用此扩展类库。并且你会发现，服务端的接口已有的代码不需要做任何改动，只需要增加此扩展包和添加一个新入口便可完美切换。特别在项目中途BOSS说我们要提高接口调用的安全性而改用phprpc协议时。  </p>
<h2>3.2.2 安装</h2>
<h3>(1)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>PHPRPC</strong> 微信包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>PHPRPC</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/PHPRPC/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！</p>
<h2>3.2.3 入门使用</h2>
<h3>(1)phprpc协议入口</h3>
<p>可以参考下面的入口代码编写：</p>
<pre><code class="language-javascript">//$ vim ./Public/phprpc/index.php

&lt;?php

require_once dirname(__FILE__) . '/../init.php';

//装载你的接口
DI()-&gt;loader-&gt;addDirs(array('Demo', 'Library'));

$server = new PHPRPC_Lite();
$server-&gt;response();</code></pre>
<h2>3.2.4 示例</h2>
<p>这里以?service=User.GetBaseInfo为例进行说明。</p>
<h3>(1)浏览器访问失败</h3>
<p>在使用了phprpc协议后，我们再以浏览器（HTTP协议）访问调用接口时，会预期地出现以下失败信息：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_3ee80900ce60ca4b1ab5facd5bc4450c" alt="0214" />  </p>
<h3>(2)通过phprpc协议调用</h3>
<p>如果通过phprpc协议调用，我们则可以得到接口返回的 <strong>源数据</strong> 。<br />
假设请求的接口为：?service=User.GetBaseInfo&amp;user_id=1，则可以得到我们熟悉的：</p>
<pre><code class="language-javascript">array (
  'ret' =&gt; 200,
  'data' =&gt; 
  array (
    'code' =&gt; 0,
    'msg' =&gt; '',
    'info' =&gt; 
    array (
      'id' =&gt; '1',
      'name' =&gt; 'dogstar',
      'note' =&gt; 'oschina',
    ),
  ),
  'msg' =&gt; '',
)</code></pre>
<h2>3.2.5 phprpc协议调试</h2>
<p>同样，为了方便进行phprpc协议下接口调用的调试，我们提供了一个脚本，如上面的对?service=User.GetBaseInfo&amp;user_id=1调用，使用脚本即为：</p>
<pre><code class="language-javascript">$ php ./Library/PHPRPC/check.php http://dev.phprpc.com/phprpc/?service=User.GetBaseInfo&amp;userId=1
array(3) {
  ["ret"]=&gt;
  int(200)
  ["data"]=&gt;
  array(3) {
    ["code"]=&gt;
    int(0)
    ["msg"]=&gt;
    string(0) ""
    ["info"]=&gt;
    array(3) {
      ["id"]=&gt;
      string(1) "1"
      ["name"]=&gt;
      string(7) "dogstar"
      ["note"]=&gt;
      string(7) "oschina"
    }
  }
  ["msg"]=&gt;
  string(0) ""
}</code></pre>
<h2>3.2.6 对客户端的调整</h2>
<p>虽然服务端不需要作出太多的改动，但对于客户端来说，仍然需要进行三方面的调整以进行phprpc协议的调用以及参数的传递和返回结果的获取。  </p>
<p>现分说如下。</p>
<h3>(1)调用方式的改变</h3>
<p>首当其冲的就是调用方式的改变，但值得开心的是，phprpc对很多语言都有支持。具体可以查看phprpc官网。</p>
<h3>(2)POST参数传递方式的改变</h3>
<p>其实对POST参数传递的改变。考虑到phprpc协议中对POST的数据有一定的复杂性，这里统一作了简化。
正如我们下面的代码所示：</p>
<pre><code class="language-javascript">    public function response($params = NULL) {
        $paramsArr = json_decode($params, TRUE);
        if ($paramsArr !== FALSE) {
            DI()-&gt;request = new PhalApi_Request(array_merge($_GET, $paramsArr));
        }
</code></pre>
<p>我们约定： <strong>通过第一个参数用JSON格式来传递全部原来需要POST的数据</strong> 。  </p>
<p>当POST的数据和GET的数据冲突时，以POST为准。  </p>
<p>所以，相应地，当需要传递POST参数时，客户需要这样调整（如PHP下）：</p>
<pre><code class="language-javascript">$client-&gt;response(json_encode($params)))</code></pre>
<p>如无此POST参数，则可以忽略不传。</p>
<h3>(3)返回结果格式的改变</h3>
<p>最后，就是返回结果格式的改变。  </p>
<p>在phprpc协议下，因为可以更轻松地获取接口返回的源数据，所以这里也同样不再通过字符串流式的序列返回（如原来的JSON或XML），而是直接返回接口的 <strong>源数据</strong> 。如上面示例中所看到的结果一样。  </p>
<p>这点，需要特别注意。</p>
<h2>3.2.7 参考</h2>
<p>phprpc官网：<a href="http://www.phprpc.org/zh_CN/">http://www.phprpc.org/zh_CN/</a></p>
<hr />
<h4><a href="/wikis/%5B3.1%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81.html">下一章</a>#[3.3]-扩展类库：基于PHPMailer的邮件发送</h4>
<h4><a href="/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">下一章</a></h4>
<hr />
<h2>3.3.1 扩展类库：基于PHPMailer的邮件发送</h2>
<p>此扩展可用于发送邮件。</p>
<h2>3.3.2 安装和配置</h2>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>PHPMailer</strong> 包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>PHPMailer</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/PHPMailer/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！接下是插件的配置。</p>
<h3>(1)邮箱配置</h3>
<p>需要在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">  'PHPMailer' =&gt; array(
    'email' =&gt; array(
        'host' =&gt; 'smtp.gmail.com',
        'username' =&gt; 'XXX@gmail.com',
        'password' =&gt; '******',
        'from' =&gt; 'XXX@gmail.com',
        'fromName' =&gt; 'PhalApi团队',
        'sign' =&gt; '&lt;br/&gt;&lt;br/&gt;请不要回复此邮件，谢谢！&lt;br/&gt;&lt;br/&gt;-- PhalApi团队敬上 ',
    ),
  ),</code></pre>
<h2>3.3.3 使用</h2>
<p>如下代码示例：</p>
<pre><code class="language-javascript">require_once './init.php';

DI()-&gt;loader-&gt;addDirs('Library');

$mailer = new PHPMailer_Lite(true);
$mailer-&gt;send('chanzonghuang@gmail.com', 'Test PHPMailer Lite', 'something here ...');</code></pre>
<p>稍候将会收到：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_6e6c7a610357cf80e4513557a110d86d" alt="123123" /></p>
<p>如果需要发送邮件给多个邮箱时，可以使用数组，如：  </p>
<pre><code>$addresses = array('chanzonghuang@gmail.com', 'test@phalapi.com');
$mailer-&gt;send($addresses, 'Test PHPMailer Lite', 'something here ...');</code></pre>
<hr />
<h4><a href="/wikis/%5B3.2%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F%E4%B8%8Bphprpc%E5%8D%8F%E8%AE%AE%E7%9A%84%E8%BD%BB%E6%9D%BE%E6%94%AF%E6%8C%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">下一章</a>#[3.4]-扩展类库：优酷开放平台接口调用</h4>
<h4><a href="/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">下一章</a></h4>
<hr />
<h2>3.4.1 扩展类库：优酷开放平台接口调用</h2>
<p>此扩展可用于对优酷开放平台提供的接口进行快速调用，并将失败的情况以debug方式写入日记。</p>
<h2>3.4.2 安装和配置</h2>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>YoukuClient</strong> 优酷接口客户端，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>YoukuClient</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/YoukuClient/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！</p>
<h2>3.4.3 使用</h2>
<p>可以先在初始化文件init.php中引入扩展类库，并注册一个youkuClient的服务：</p>
<pre><code class="language-javascript">//$ vim ./Public/init.php
$loader-&gt;addDirs('Library');

DI()-&gt;youkuClient = function() {
    return new YoukuClient_Lite('https://openapi.youku.com', 'b043a60fbef8aed0');
};</code></pre>
<p>然后，就可以在项目中使用youkuClient这个服务来实现对优酷平台接口的调用。  </p>
<p>通常，我们会在Model层包装这一层数据来源的获取。但这里只是了为演示的效果，全部将代码放到Api里面，如：</p>
<pre><code class="language-javascript">//$ vim ./Demo/Api/Youku.php 

&lt;?php

class Api_Youku extends PhalApi_Api {

    public function showBasic()
    {
        $uri = '/v2/videos/show_basic.json';
        $params = array(
            'video_url' =&gt; 'http://v.youku.com/v_show/id_XOTA4ODU4NjA0.html'
        );
        $timeoutMs = 3000;

        $info = DI()-&gt;youkuClient-&gt;get($uri, $params, $timeoutMs);

        return array('id' =&gt; $info['id'], 'title' =&gt; $info['title']);
    }
}</code></pre>
<p>请求一下对应的接口，便可以获得以下的运行效果：  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_d7386899f4fbd38cb5493950026a5b02" alt="0310" /></p>
<p>对应的优酷视频是： <a href="http://v.youku.com/v_show/id_XOTA4ODU4NjA0.html?f=23542444&amp;ev=4&amp;from=y1.1-2.10001-0.1-1">【双语字幕】Apple Watch 动手玩 - The Verge</a></p>
<hr />
<h4><a href="/wikis/%5B3.3%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E5%9F%BA%E4%BA%8EPHPMailer%E7%9A%84%E9%82%AE%E4%BB%B6%E5%8F%91%E9%80%81.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">下一章</a>#[3.5]-扩展类库：七牛云存储接口调用</h4>
<h4><a href="/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.6%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">下一章</a></h4>
<hr />
<h2>3.5.1 扩展类库：七牛云存储接口调用</h2>
<p>此扩展可用于将图片上传到七牛云存储，或者七牛提供的其他功能。</p>
<h2>3.5.2 安装和配置</h2>
<h3>(1)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>Qiniu</strong> 七牛扩展包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>Qiniu</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/Qiniu/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！接下是插件的配置。</p>
<h3>(2)扩展包配置</h3>
<p>我们需要在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">    /**
     * 七牛相关配置
     */
    'Qiniu' =&gt;  array(
        //统一的key
        'accessKey' =&gt; '*****',
        'secretKey' =&gt; '****',
        //自定义配置的空间
        'space_bucket' =&gt; '自定义配置的空间',
        'space_host' =&gt; 'http://XXXXX.qiniudn.com',
    ),</code></pre>
<p>你也可以不使用配置文件，直接在初始化注册DI()-&gt;qiniu时指定配置。</p>
<h2>3.5.3 入门使用</h2>
<h3>(1)入口注册</h3>
<pre><code class="language-javascript">//$ vim ./Public/init.php

// 其他代码....

DI()-&gt;qiniu = new Qiniu_Lite();</code></pre>
<p>根据上面所说的，你可以也可以指定配置。</p>
<pre><code class="language-javascript">//$ vim ./Public/init.php

// 其他代码....

$qiniuConfig = array( /* 格式参照配置文件 *);
DI()-&gt;qiniu = new Qiniu_Lite($qiniuConfig);</code></pre>
<h2>3.5.4 示例：图片上传</h2>
<h3>(1)模拟客户端</h3>
<p>先简单写个测试文件：</p>
<pre><code class="language-javascript">// vim ./Public/test_qiniu.html 

&lt;html&gt;
    &lt;form method="POST" action="/demo/?service=Qiniu_CDN.uploadFile" enctype="multipart/form-data"&gt;
        &lt;input type="file" name="file"&gt;
        &lt;input type="submit"&gt;
    &lt;/form&gt;
&lt;/html&gt;</code></pre>
<p>效果如下（通常是提供给客户端上传的）：<br />
<img src="http://webtools.qiniudn.com/20150411005257_d89f36ff2e6956e31226cf245ab0b6d1" alt="p" /></p>
<h3>(2)文件上传接口</h3>
<p>随后，我们使用此七牛扩展提供的编写文件上传接口，通过查看在线接口文档，可以看到：<br />
<img src="http://7xslqv.com1.z0.glb.clouddn.com/images/9/65/39868592d7ef88e4e2c3a7c0c3778.png" alt="" /></p>
<h3>(3)运行一下：</h3>
<p>缺少上传文件时：</p>
<pre><code class="language-javascript">{"ret":200,"data":{"code":1,"url":"","msg":"miss upload file"},"msg":""}</code></pre>
<p>上传失败时：</p>
<pre><code class="language-javascript">{"ret":200,"data":{"code":2,"url":"","msg":"fail to upload file"},"msg":""}</code></pre>
<p>可以看到对应的日记（如果不想看到此日记，可通过在初始化日记服务时去掉调试日记）：</p>
<pre><code class="language-javascript">2015-03-18 00:07:02|DEBUG|failed to upload file to qiniu|{"Err":"bad token","Reqid":"u1EAAG73DB3PVMwT","Details":"","Code":401}</code></pre>
<p>上传成功时：</p>
<pre><code class="language-javascript">{"ret":200,"data":{"code":0,"url":"图片链接","msg":""},"msg":""}</code></pre>
<hr />
<h4><a href="/wikis/%5B3.4%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.6%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">下一章</a>#[3.6]-扩展类库：新型计划任务</h4>
<h4><a href="/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a></h4>
<hr />
<h2>3.6.1 扩展类库：新型计划任务</h2>
<p>此扩展类型用于后台计划任务的调度，主要功能点有：</p>
<ul>
<li>1、提供了Redis/文件/数据库三种MQ队列</li>
<li>2、提供了本地和远程两种调度方式</li>
<li>3、以接口的形式实现计划任务</li>
<li>4、提供统一的crontab调度</li>
</ul>
<p>更多内容，请查看：<a href="/wikis/%5B1.31%5D-%E6%96%B0%E5%9E%8B%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%9A%E4%BB%A5%E6%8E%A5%E5%8F%A3%E5%BD%A2%E5%BC%8F%E5%AE%9E%E7%8E%B0%E7%9A%84%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1.html">[1.31] 新型计划任务：以接口形式实现的计划任务</a>  </p>
<h2>3.6.2 安装</h2>
<h3>(1)内置扩展包</h3>
<p>此安装略，已内置于PhalApi框架中。</p>
<blockquote>
<p>温馨提示：<br />
PhalApi需要PhaApi 1.2.0及以上版本。</p>
</blockquote>
<h3>(2)配置</h3>
<p>我们需要在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">    /**
     * 计划任务配置
     */
    'Task' =&gt; array(
        //MQ队列设置，可根据使用需要配置
        'mq' =&gt; array(
            'file' =&gt; array(
                'path' =&gt; API_ROOT . '/Runtime',
                'prefix' =&gt; 'phalapi_task',
            ),
            'redis' =&gt; array(
                'host' =&gt; '127.0.0.1',
                'port' =&gt; 6379,
                'prefix' =&gt; 'phalapi_task',
                'auth' =&gt; '',
            ),
            'mc' =&gt; array(
                'host' =&gt; '127.0.0.1',
                'port' =&gt; 11211,
            ),
        ),

        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' =&gt; array(
            'remote' =&gt; array(
                'host' =&gt; 'http://library.phalapi.net/demo/',
                'timeoutMS' =&gt; 3000,
            ),
        ),
    ),</code></pre>
<p>以上内容看情况需要而配置。  </p>
<h3>(3)数据库表</h3>
<p>当需要使用数据库MQ列队时，还需要将以下数据库的配置追加到 <strong>./Config/dbs.php</strong> 中：</p>
<pre><code class="language-javascript">    'tables' =&gt; array(

    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' =&gt; array(
            'prefix' =&gt; 'phalapi_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
                array('start' =&gt; 0, 'end' =&gt; 9, 'db' =&gt; 'db_demo'),
            ),
        ),
    )</code></pre>
<p>同时，需要将 ./Data/phalapi_task_mq.sql 文件的SQL建表语句导入到你的数据库。你也可以将利用脚本来生成。</p>
<h2>3.6.3 入门使用</h2>
<h3>(1)加入MQ队列</h3>
<p>首先，我们需要在入口文件进行对Task的初始化：</p>
<pre><code class="language-javascript">DI()-&gt;loader-&gt;addDirs('Library');
$mq = new Task_MQ_Redis();  //可以选择你需要的MQ
DI()-&gt;taskLite = new Task_Lite($mq);</code></pre>
<p>然后，这样即可添加一个新的计划任务到MQ：</p>
<pre><code class="language-javascript">DI()-&gt;taskLite-&gt;add('Task.DoSth', array('id' =&gt; 1));</code></pre>
<h3>(2)计划任务的启动</h3>
<p>在启动计划任务前，我们需要编写简单的脚本，一如这样：</p>
<pre><code class="language-javascript">#!/usr/bin/env php
&lt;?php
require_once '/path/to/Public/init.php';

DI()-&gt;loader-&gt;addDirs('Demo');

if ($argc &lt; 2) {
    echo "Usage: $argv[0] &lt;service&gt; \n\n";
    exit(1);
}

$service = trim($argv[1]);

$mq = new Task_MQ_Redis();
$runner = new Task_Runner_Local($mq);
$rs = $runner-&gt;go($service);

echo "\nDone:\n", json_encode($rs), "\n\n";</code></pre>
<p>然后使用nohup或者crontab启动即可。  </p>
<h3>(3)续篇 - 统一的crontab计划任务</h3>
<p>上面第二点的启动是通用、自定义、也是自由的启动方式。  </p>
<p>这里再提供一种具体的、统一的启动方式，即使用crontab的方式。  </p>
<p>首先，创建以下表(或见./Library/Task/Data/phalapi_task_progress.sql文件)：</p>
<pre><code class="language-javascript">CREATE TABLE `phalapi_task_progress` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `title` varchar(200) DEFAULT '' COMMENT '任务标题',
      `trigger_class` varchar(50) DEFAULT '' COMMENT '触发器类名',
      `fire_params` varchar(255) DEFAULT '' COMMENT '需要传递的参数，格式自定',
      `interval_time` int(11) DEFAULT '0' COMMENT '执行间隔，单位：秒',
      `enable` tinyint(1) DEFAULT '1' COMMENT '是否启动，1启动，0禁止',
      `result` varchar(255) DEFAULT '' COMMENT '运行的结果，以json格式保存',
      `state` tinyint(1) DEFAULT '0' COMMENT '进程状态，0空闲，1运行中，-1异常退出',
      `last_fire_time` int(11) DEFAULT '0' COMMENT '上一次运行时间',
      PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre>
<p>接着，添加crontab计划任务：</p>
<pre><code class="language-javascript">$ crontab -e

*/1 * * * * /usr/bin/php /path/to/PhalApi/Library/Task/crontab.php &gt;&gt; /tmp/phalapi_task_crontab.log 2&gt;&amp;1
</code></pre>
<p>然后，实现你的计划任务服务接口：</p>
<pre><code class="language-javascript">&lt;?php
class Api_Task_Demo extends PhalApi_Api {

      public function doSth() {
            // ...
      }
}</code></pre>
<p>最后，配置计划任务：</p>
<pre><code class="language-javascript">INSERT INTO `phalapi_task_progress`(title, trigger_class, fire_params, interval_time)  VALUES('你的任务名字', 'Task_Progress_Trigger_Common', 'Task_Demo.DoSth&amp;Task_MQ_File&amp;Task_Runner_Local', '300');</code></pre>
<p>注意，在配置时，需要指明MQ和Runner的类型。</p>
<h2>3.6.4 更多说明</h2>
<h3>(1)依赖的资源服务</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th>类</th>
<th>没有时是否自动创建</th>
</tr>
</thead>
<tbody>
<tr>
<td>curl</td>
<td>PhalApi_CUrl</td>
<td>是</td>
</tr>
<tr>
<td>request</td>
<td>PhalApi_Request</td>
<td>强制每次初始化，用于本地调度时</td>
</tr>
<tr>
<td>response</td>
<td>PhalApi_Response_Json</td>
<td>强制每次初始化，用于本地调度时</td>
</tr>
</tbody>
</table>
<h3>(2)MQ</h3>
<h4>Redis MQ队列</h4>
<p>Redis MQ队列需要的配置为：</p>
<pre><code class="language-javascript">    'Task' =&gt; array(
        //MQ队列设置，可根据使用需要配置
        'mq' =&gt; array(
            'redis' =&gt; array(
                'host' =&gt; '127.0.0.1',
                'port' =&gt; 6379,
                'prefix' =&gt; 'phalapi_task',
                'auth' =&gt; '',
            ),
        ),
    ),</code></pre>
<p>其中：  </p>
<table>
<thead>
<tr>
<th>选项</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>host</td>
<td>否</td>
<td>127.0.0.1</td>
<td>redis的HOST</td>
</tr>
<tr>
<td>port</td>
<td>否</td>
<td>6379</td>
<td>redis的端口</td>
</tr>
<tr>
<td>prefix</td>
<td>否</td>
<td>phalapi_task</td>
<td>key的前缀</td>
</tr>
<tr>
<td>auth</td>
<td>否</td>
<td>redis的验证，不为空时执行验证</td>
</tr>
</tbody>
</table>
<p>可以这样创建Redis MQ队列：</p>
<pre><code class="language-javascript">//方法一：使用app.Task.mq.redis配置
$mq = new Task_MQ_Redis();

//方法二：外部依赖注入
$redisCache = new PhalApi_Cache_Redis(array('host' =&gt; '127.0.0.1'));
$mq = new Task_MQ_Redis($redisCache);</code></pre>
<h4>文件MQ队列（通常不能共享，队列大小不限制，有效期一年）</h4>
<p>文件MQ需要的配置为：</p>
<pre><code class="language-javascript">    'Task' =&gt; array(
        //MQ队列设置，可根据使用需要配置
        'mq' =&gt; array(
            'file' =&gt; array(
                'path' =&gt; API_ROOT . '/Runtime',
                'prefix' =&gt; 'phalapi_task',
            ),
        ),
    ),</code></pre>
<p>其中：  </p>
<table>
<thead>
<tr>
<th>选项</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>path</td>
<td>否</td>
<td>API_ROOT/Runtime</td>
<td>缓存的文件目录</td>
</tr>
<tr>
<td>prefix</td>
<td>否</td>
<td>phalapi_task</td>
<td>key的前缀</td>
</tr>
</tbody>
</table>
<p>可以这样创建文件MQ队列：</p>
<pre><code class="language-javascript">//方法一：使用app.Task.mq.file配置
$mq = new Task_MQ_File();

//方法二：外部依赖注入
$fileCache = new PhalApi_Cache_File(array('path' =&gt; '/tmp/cache'));
$mq = new Task_MQ_File($fileCache);</code></pre>
<h4>Memcached/Memcache MQ队列（通常队列大小不能超过1M，有效期29天）</h4>
<p>MC MQ需要的配置为：</p>
<pre><code class="language-javascript">    'Task' =&gt; array(
        //MQ队列设置，可根据使用需要配置
        'mq' =&gt; array(
            'mc' =&gt; array(
                'host' =&gt; '127.0.0.1',
                'port' =&gt; 11211,
            ),
        ),
    ),</code></pre>
<p>其中：  </p>
<table>
<thead>
<tr>
<th>选项</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>host</td>
<td>否</td>
<td>127.0.0.1</td>
<td>MC的host</td>
</tr>
<tr>
<td>port</td>
<td>否</td>
<td>11211</td>
<td>MC端口</td>
</tr>
</tbody>
</table>
<p>可以这样创建文件MQ队列：</p>
<pre><code class="language-javascript">//方法一：使用app.Task.mq.mc配置
$mq = new Task_MQ_Memcached();

//方法二：外部依赖注入
$mc = new PhalApi_Cache_Memcached(array('host' =&gt; '127.0.0.1', 'port' =&gt; 11211));
$mq = new Task_MQ_File($mc);</code></pre>
<h4>数据库MQ队列</h4>
<p>数据库MQ队列需要的配置为：</p>
<pre><code class="language-javascript">    'tables' =&gt; array(

        //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中，未配置的表将使用默认路由

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'task_mq' =&gt; array(
            'prefix' =&gt; 'phalapi_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
                array('start' =&gt; 0, 'end' =&gt; 9, 'db' =&gt; 'db_demo'),
            ),
        ),
    )</code></pre>
<p>可以这样创建数据库MQ队列：</p>
<pre><code class="language-javascript">$mq = new Task_MQ_DB();</code></pre>
<h4>数组MQ队列</h4>
<p>数组MQ队列是将MQ存放在PHP的数组里面，用于单元测试或者是一次性、临时性的计划任务调度。</p>
<p>可以这样创建数据库MQ队列：</p>
<pre><code class="language-javascript">$mq = new Task_MQ_Array();</code></pre>
<h3>(3)本地和远程调度</h3>
<h4>本地调度的创建</h4>
<pre><code class="language-javascript">$runner = new Task_Runner_Local($mq, 10);  //10表示每批次弹出10个进行处理</code></pre>
<p>需要注意的是，每次执行一个计划任务，都会重新初始化必要的DI资源服务。<br />
且此调度方式不能用于接口请求时的同步调用。  </p>
<h4>远程调度的创建</h4>
<p>首先需要以下配置：</p>
<pre><code class="language-javascript">    /**
     * 计划任务配置
     */
    'Task' =&gt; array(
        //Runner设置，如果使用远程调度方式，请加此配置
        'runner' =&gt; array(
            'remote' =&gt; array(
                'host' =&gt; 'http://library.phalapi.net/demo/',
                'timeoutMS' =&gt; 3000,
            ),
        ),
    ),</code></pre>
<p>其中：  </p>
<table>
<thead>
<tr>
<th>选项</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>host</td>
<td>是</td>
<td>接口域名链接</td>
</tr>
<tr>
<td>timeoutMS</td>
<td>否</td>
<td>3000</td>
<td>接口超时时间，单位毫秒</td>
</tr>
</tbody>
</table>
<p>可以这样创建：</p>
<pre><code class="language-javascript">//使用默认的连接器 - HTTP + POST的方式
$runner = new Task_Runner_Remote($mq, 10); //10表示每批次弹出10个进行处理

//或者，指定连接器
$connector = new Task_Runner_Remote_Connector_Impl();
$runner = new Task_Runner_Remote($mq, 10, $connector); //10表示每批次弹出10个进行处理</code></pre>
<hr />
<h4><a href="/wikis/%5B3.5%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E4%B8%83%E7%89%9B%E4%BA%91%E5%AD%98%E5%82%A8%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a>#[3.8]-扩展类库：用户、会话和第三方登录集成</h4>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.9%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9Aswoole%E6%94%AF%E6%8C%81%E4%B8%8B%E7%9A%84%E9%95%BF%E9%93%BE%E6%8E%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E5%AE%9E%E7%8E%B0.html">下一章</a></h4>
<hr />
<p><em>“你应该明白，我的朋友，<br />
宇宙从来无所谓起始，<br />
它也永远不会有结局，<br />
因为邦迪，戈尔德，还有我，<br />
直到我们头发掉精光，<br />
对这一点都深信不疑！”</em>  </p>
<p><em>--《物理世界奇遇记》</em></p>
<h2>3.8.1 扩展类库：用户、会话和第三方登录集成</h2>
<p>首先，特别感谢@Aevit ，允许我将些扩展类库进行开源。原来此类库的功能只是当前开发项目中的功能，我现将其抽离成可配置使用的扩展类库，以供大家共享。  </p>
<p>此类库主要特点有：</p>
<ul>
<li>1、可以和第三方登录集成，包括：微信登录、新浪登录、QQ登录</li>
<li>2、为客户端提供了直接可以调用的登录接口</li>
<li>3、为服务端提供了直接可以检测用户登录态的操作</li>
<li>4、支持token落地、高效缓存和分布式的数据库存储  </li>
<li>5、展示了如何开发一个项目级的类库、包括数据库配置、翻译等</li>
</ul>
<blockquote>
<p>温馨提示：<br />
此扩展类库还在开发完善中，但已有项目在使用，感兴趣的同学可以尝试使用。  </p>
</blockquote>
<h2>3.8.2 安装</h2>
<h3>(1)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>User</strong> 用户包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>User</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/User ./PhalApi/Library/ -R</code></pre>
<h3>(2)数据库表导入</h3>
<p>默认下，User类库会使用默认的数据库配置，但有追加一些需要用到的表。因此需要导入以下表：</p>
<pre><code class="language-javascript">$ cd /Library/User
$ tree

├── Data
│   ├── user_login_qq.sql
│   ├── user_login_sina.sql
│   ├── user_login_weixin.sql
│   ├── user_session.sql
│   └── user.sql
</code></pre>
<blockquote>
<p>温馨提示：<br />
导入前，可以自行调整表的前缀。</p>
</blockquote>
<h3>(3)数据库配置</h3>
<p>将 PhalApi-Library/User/Config/dbs.php 里面的配置拷贝到你的项目配置：</p>
<pre><code class="language-javascript">    //请将以下配置拷贝到 ./Config/dbs.php 文件对应的位置中

        //10张表，可根据需要，自行调整表前缀、主键名和路由
        'user_session' =&gt; array(
            'prefix' =&gt; 'phalapi_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('db' =&gt; 'db_demo'),
                array('start' =&gt; 0, 'end' =&gt; 9, 'db' =&gt; 'db_demo'),
            ),
        ),</code></pre>
<h3>(4)入口注册</h3>
<pre><code class="language-javascript">//必须显式注册，以便可以让服务自行初始化
DI()-&gt;userLite = new User_Lite();</code></pre>
<h3>(5)其他可选配置</h3>
<ul>
<li>为了体现高效缓存的好处，建议先注册DI()-&gt;cache</li>
<li>为了接口更明朗的参数规则说明，建议将下而规则追加到配置文件./Config/app.php中：</li>
</ul>
<pre><code class="language-javascript">return array(

    /**
     * 应用接口层的统一参数
     */
    'apiCommonRules' =&gt; array(
        //其他原来的参数配置
        ...

        //登录信息
        'userId' =&gt; array(
            'name' =&gt; 'user_id', 'type' =&gt; 'int', 'default' =&gt; 0, 'require' =&gt; false,
        ),
        'token' =&gt; array(
            'name' =&gt; 'token', 'type' =&gt; 'string', 'default' =&gt; '', 'require' =&gt; false,
        ),
    ),</code></pre>
<h3>(6)checkApiParams.php的初始化</h3>
<p>同样，需要在checkApiParams.php文件显式注册，才能让User这个扩展类库完成自我的必要初始化：</p>
<pre><code class="language-javascript">//装载你的接口
DI()-&gt;loader-&gt;addDirs(array('Demo', 'Library'));
DI()-&gt;userLite = new User_Lite();</code></pre>
<p>否则会导致service不存在。</p>
<h2>3.8.3 入门使用</h2>
<h3>(1)对于客户端</h3>
<h4>(1-1)微信登录</h4>
<h5>1、功能说明</h5>
<p>实现通过微信登录，并且首次登录时，自动创建用户并绑定。</p>
<h5>2、接口URL</h5>
<p>/?service=User_User_Login.weixin + 公共参数(免登录态)</p>
<h5>3、接口参数</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>wx_openid</td>
<td>1</td>
<td>微信OPENID</td>
</tr>
<tr>
<td>wx_token</td>
<td>1</td>
<td>微信TOKEN</td>
</tr>
<tr>
<td>wx_expires_in</td>
<td>1</td>
<td>微信失效时间</td>
</tr>
</tbody>
</table>
<h5>4、返回结果</h5>
<h5>返回字段</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>info</td>
<td>object</td>
<td>登录成功后的相关信息</td>
</tr>
<tr>
<td>user_id</td>
<td>int</td>
<td>用户ID</td>
</tr>
<tr>
<td>token</td>
<td>string</td>
<td>用户TOKEN</td>
</tr>
<tr>
<td>is_new</td>
<td>int</td>
<td>是否为首次登录，0不是，1是</td>
</tr>
</tbody>
</table>
<h5>结果示例</h5>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}</code></pre>
<h5>请求示例</h5>
<p>/service=User_User_Login.weixin&amp;wx_openid=wx_122348561111&amp;wx_token=ASDF&amp;wx_expires_in=130000000&amp;name=weixinName&amp;avatar=phpunit.png</p>
<h4>(1-2)QQ登录</h4>
<h5>1、功能说明</h5>
<p>实现通过QQ登录，并且首次登录时，自动创建用户并绑定。</p>
<h5>2、接口URL</h5>
<p>/?service=User_User_Login.qq + 公共参数(免登录态)</p>
<h5>3、接口参数</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>qq_openid</td>
<td>1</td>
<td>QQ的OPENID</td>
</tr>
<tr>
<td>qq_token</td>
<td>1</td>
<td>QQ的TOKEN</td>
</tr>
<tr>
<td>qq_expires_in</td>
<td>1</td>
<td>QQ的失效时间</td>
</tr>
</tbody>
</table>
<h5>4、返回结果</h5>
<h5>返回字段</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>info</td>
<td>object</td>
<td>登录成功后的相关信息</td>
</tr>
<tr>
<td>user_id</td>
<td>int</td>
<td>用户ID</td>
</tr>
<tr>
<td>token</td>
<td>string</td>
<td>用户TOKEN</td>
</tr>
<tr>
<td>is_new</td>
<td>int</td>
<td>是否为首次登录，0不是，1是</td>
</tr>
</tbody>
</table>
<h5>结果示例</h5>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}</code></pre>
<h5>请求示例</h5>
<p>/service=User_User_Login.qq&amp;qq_openid=xxxx&amp;qq_token=ASDF&amp;qq_expires_in=130000000&amp;name=sinaName&amp;avatar=<a href="http://dev.phalapi.com/no_avatar.png">http://dev.phalapi.com/no_avatar.png</a></p>
<h4>(1-3)新浪微博登录</h4>
<h5>1、功能说明</h5>
<p>实现通过新浪微博登录，并且首次登录时，自动创建用户并绑定。</p>
<h5>2、接口URL</h5>
<p>/?service=User_User_Login.sina</p>
<h5>3、接口参数</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>sina_openid</td>
<td>1</td>
<td>新浪微博OPENID</td>
</tr>
<tr>
<td>sina_token</td>
<td>1</td>
<td>新浪微博TOKEN</td>
</tr>
<tr>
<td>sina_expires_in</td>
<td>1</td>
<td>新浪微博失效时间</td>
</tr>
</tbody>
</table>
<h5>4、返回结果</h5>
<h5>返回字段</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>info</td>
<td>object</td>
<td>登录成功后的相关信息</td>
</tr>
<tr>
<td>user_id</td>
<td>int</td>
<td>用户ID</td>
</tr>
<tr>
<td>token</td>
<td>string</td>
<td>用户TOKEN</td>
</tr>
<tr>
<td>is_new</td>
<td>int</td>
<td>是否为首次登录，0不是，1是</td>
</tr>
</tbody>
</table>
<h5>结果示例</h5>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,  //0正常登录
        "info": {
            "user_id": 3,  //用户ID
            "token": "430C0F31FAF1FB1565E4290D1B61185A2408A6DEEA1604C1B5AEB14E44BDF2E0",  //用户TOKEN
            "is_new": 0   //是否为首次登录，0不是，1是
        },
        "msg": ""
    },
    "msg": ""
}</code></pre>
<h5>请求示例</h5>
<p>/service=User_User_Login.weixin&amp;wx_openid=wx_122348561111&amp;wx_token=ASDF&amp;wx_expires_in=130000000&amp;name=weixinName&amp;avatar=phpunit.png</p>
<h4>(1-4)用户信息</h4>
<h5>1、功能说明</h5>
<p>获取用户个人信息</p>
<h5>2、接口URL</h5>
<p>/?service=User_User_Info.getUserInfo + 公共参数</p>
<h5>3、接口参数</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>other_user_id</td>
<td>1</td>
<td>用户id</td>
</tr>
</tbody>
</table>
<h5>4、返回结果</h5>
<h5>返回字段</h5>
<table>
<thead>
<tr>
<th>参数</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>info</td>
<td>object</td>
<td>登录成功后的相关信息</td>
</tr>
<tr>
<td>user_id</td>
<td>int</td>
<td>用户ID</td>
</tr>
<tr>
<td>username</td>
<td>string</td>
<td>用户名</td>
</tr>
<tr>
<td>nickname</td>
<td>string</td>
<td>用户昵称</td>
</tr>
<tr>
<td>avatar</td>
<td>string</td>
<td>用户头像url</td>
</tr>
</tbody>
</table>
<h5>结果示例</h5>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "username": "aevit",
            "nickname": "test",
            "avatar": "",
            "user_id": 1
        },
        "msg": ""
    },
    "msg": ""
}</code></pre>
<h5>请求示例</h5>
<p>/service=User_User_Info.getUserInfo&amp;other_user_id=1</p>
<h3>(2)对于服务端</h3>
<h4>(1)温柔式检测登录态</h4>
<pre><code class="language-javascript">$isLogin = DI()-&gt;userLite-&gt;check();</code></pre>
<h4>(2)强制式检测登录态</h4>
<pre><code class="language-javascript">DI()-&gt;userLite-&gt;check(true);</code></pre>
<h4>(3)更多操作</h4>
<p>当需要对用户进行退出登录时，可以在接口中执行：  </p>
<pre><code>DI()-&gt;userLite-&gt;logout();</code></pre>
<p>当需要为用户新建一个会话时，可以在接口中执行：  </p>
<pre><code>DI()-&gt;userLite-&gt;generateSession(1); //假设用户ID为1</code></pre>
<p>当需要为用户延长登录态有效时间时（默认为一个月），可以在接口进行心跳操作，从而再延长一个月有效时间：  </p>
<pre><code>DI()-&gt;userLite-&gt;heartbeat();</code></pre>
<h2>3.8.4 示例</h2>
<h3>(1)假设在我们的应用的接口中：</h3>
<pre><code class="language-javascript">&lt;?php

class Api_Test extends PhalApi_Api {

    public function go() {
        Di()-&gt;userLite-&gt;check(true);

        return 'ok';
    }
}
</code></pre>
<h3>(2)先调用微信的登录：</h3>
<pre><code>/user/?service=User_User_Login.Weixin&amp;wx_openid=wx_122348561111&amp;wx_token=ASDF&amp;wx_expires_in=130000000&amp;name=weixinName&amp;avatar=phpunit.png</code></pre>
<p>返回：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,
        "info": {
            "user_id": 94,
            "token": "8509BE9C80B67009C559E6A2C4B88E02C885DA6B9FEFFC2DA080999377D08FBF",
            "is_new": 0
        },
        "msg": ""
    },
    "msg": ""
}</code></pre>
<h3>(3)再调用我们应用 的接口：</h3>
<pre><code>/user/?service=Test.go&amp;user_id=94&amp;token=CE1218493B3A441FCF630E24E98FD3640133EBA6D734C90A620A11C07CE35323</code></pre>
<p>返回：</p>
<pre><code class="language-javascript">{"ret":200,"data":"ok","msg":""}</code></pre>
<h3>(4)未登录下，返回：</h3>
<pre><code class="language-javascript">{"ret":401,"data":[],"msg":"\u975e\u6cd5\u8bf7\u6c42\uff1auser not login"}</code></pre>
<hr />
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B3.9%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9Aswoole%E6%94%AF%E6%8C%81%E4%B8%8B%E7%9A%84%E9%95%BF%E9%93%BE%E6%8E%A5%E5%92%8C%E5%BC%82%E6%AD%A5%E4%BB%BB%E5%8A%A1%E5%AE%9E%E7%8E%B0.html">下一章</a>#[3.9]-扩展类库：swoole支持下的长链接和异步任务实现</h4>
<h4><a href="/wikis/%5B3.8%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E7%94%A8%E6%88%B7%E3%80%81%E4%BC%9A%E8%AF%9D%E5%92%8C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%BD%95%E9%9B%86%E6%88%90.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a></h4>
<hr />
<p><em>遵守规则可以让你远离选择烦恼，无论是在坐车还是参加鸡尾酒会的时候。 --《选择的悖论》</em>  </p>
<h2>写在前面的话</h2>
<p>此PhalApi扩展类库只是初步开发完成，建议有swoole扩展经验或非保守派的同学使用，也欢迎你来丰富完善此扩展类库。</p>
<h2>3.9.1 扩展类库：swoole支持下的长链接和异步任务实现</h2>
<p>swoole官网请见： <a href="http://www.swoole.com/">Swoole: PHP的异步、并行、分布式扩展</a>  </p>
<p>在这里，首先需要非常感谢swoole。  </p>
<p>因为Swoole给我们提供了很多解决以往因PHP本身限制而产生的难题的灵感，但与此同时，也给我们带来了从没遇到过的挑战，特别是并发和长时间运行。  </p>
<p>这需要我们更为小心地进行编码，因为以往可以忽略的小问题，在新的解决方案背景下都可能成为一个大问题。此扩展类库更多是作为一种尝试，并且可以作为扩展类库重要的一个转换点。因为在今后的日子里， <strong>PhalApi将会争取与其他开源项目一起，提供企业级的解决方案</strong> 。  </p>
<p>目前，此扩展类库提供了：</p>
<ul>
<li>长链接的接口调用</li>
<li>异步计划任务的调用</li>
</ul>
<h2>3.9.2 安装</h2>
<p>(1)安装swoole扩展类库
安装过程可以参考swoole官网，这里稍以linux系统简单说明一下编译安装。</p>
<pre><code class="language-javascript">unzip ./swoole-src-swoole-1.7.16-beta.zip
cd swoole-src-swoole-1.7.16-beta
phpize
./configure
make &amp;&amp; make install</code></pre>
<p>安装好后，添加swoole扩展：</p>
<pre><code class="language-javascript">extension=swoole.so</code></pre>
<p>重启PHP后，如果看到有swoole扩展，则说明安装成功：</p>
<pre><code class="language-javascript">#php -m | grep swoole
swoole</code></pre>
<h3>(2)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>Swoole</strong> 包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>Swoole</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/Swoole/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！ </p>
<blockquote>
<p>温馨提示：<br />
此扩展类库需要PhaApi 1.1.4及以上版本。</p>
</blockquote>
<h3>(3)配置</h3>
<p>将以下配置追加到./Config/app.php：</p>
<pre><code class="language-javascript">    /**
     * Swoole扩展类库
     */
    'Swoole' =&gt; array(
        //服务
        'server' =&gt; array(
            'ip' =&gt; '127.0.0.1',
            'port' =&gt; 9501,
            'worker_num' =&gt; 1,
        ),
        //计划任务
        'task' =&gt; array(
            'ip' =&gt; '127.0.0.1',
            'port' =&gt; 9502,
            'worker_num' =&gt; 1,
        ),
    ),</code></pre>
<h2>3.9.3 入门使用</h2>
<h3>(1)长链接入口</h3>
<p>在使用长链接入口对外提供接口服务后，由于不再是HTTP协议，所以入口建议放置在新的目录./Server，而不再是./Public。<br />
入口文件的编写，如同我们以往一样，很简单：</p>
<pre><code class="language-javascript">//$ vim ./Server/run_server.php
&lt;?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()-&gt;loader-&gt;addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite-&gt;runServer();
</code></pre>
<h4>启动、重启和关闭服务</h4>
<p>启动可以用：</p>
<pre><code class="language-javascript">php ./Server/run_server.php</code></pre>
<p>关闭可以用：</p>
<pre><code class="language-javascript">ps -ef | grep run_server | grep -v grep | awk '{print $2}'|xargs kill -9</code></pre>
<h3>(2)异步计划任务</h3>
<p>异步计划任务是新型的做法，即：也通过接口服务调用的方式来完成计划任务的调度，其启动文件如同长链接入口一样简单：</p>
<pre><code class="language-javascript">//$ vim ./Server/run_task.php
&lt;?php

require_once dirname(__FILE__) . '/../Public/init.php';

DI()-&gt;loader-&gt;addDirs(array('Library', 'Demo'));

$swooleLite = new Swoole_Lite();
$swooleLite-&gt;runTask();</code></pre>
<h4>启动、重启和关闭服务</h4>
<p>启动可以用：</p>
<pre><code class="language-javascript">nohup php ./Server/run_task.php &gt; ./Server/run_task.log 2&gt;&amp;1 &amp;</code></pre>
<h3>(3)客户端调用</h3>
<p>在扩展类库里有一个测试的脚本，可以用来进行PHP客户端的请求。</p>
<h4>(1)默认接口调用</h4>
<pre><code class="language-javascript">$ php ./check.php 127.0.0.1 9501 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Received: {"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430620911},"msg":""}
Connection close</code></pre>
<h4>(2)带数据库的调用</h4>
<pre><code class="language-javascript">$ php ./check.php 127.0.0.1 9501 User.getBaseInfo userId=1
Send: {"service":"User.getBaseInfo","userId":"1"}
Received: {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","username":"aevit","nickname":"test","password":"DE4CA99150F44B26F0D320DCA6E4B7629C43B6","salt":"wefewfew","reg_time":"0","avatar":"http:\/\/image.famillelab.com\/no_avatar.png","UUID":""}},"msg":""}
Connection close</code></pre>
<h4>(3)异步计划任务的调度</h4>
<pre><code class="language-javascript">$ php ./check.php 127.0.0.1 9502 Default.Index username=swoole
Send: {"service":"Default.Index","username":"swoole"}
Connection close</code></pre>
<p>假设Default.Index为一个计划任务的接口，但目前发现一个问题是，首次请求异步计划任务不会主动结束，而需要工作强制ctrl + c结束，再请求，则正常。  </p>
<p>对应的log，可以看到：</p>
<pre><code class="language-javascript">2015-05-03 12:24:57|DEBUG|asynctask(0) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(0) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
2015-05-03 12:24:57|DEBUG|asynctask(1) dispath in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) start in swoole|{"service":"Default.Index","username":"swoole"}
2015-05-03 12:24:57|DEBUG|asynctask(1) finish in swoole|{"ret":200,"data":{"title":"Hello World!","content":"Hi swoole, welcome to use PhalApi!","version":"1.1.3","time":1430627097},"msg":""}
...</code></pre>
<h2>3.9.4 对客户端调整</h2>
<h3>(1)调用方式的改变</h3>
<p>改用长链接。</p>
<h3>(2)POST参数传递方式的改变</h3>
<p>统一使用json发送数据包。</p>
<h2>3.9.5 对服务端的影响</h2>
<h3>(1)DI资源服务的调整</h3>
<table>
<thead>
<tr>
<th>变量名称</th>
<th>是否全局通用</th>
<th>是否每次请求新建</th>
<th>可否使用</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>loader</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>config</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>logger</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>notorm</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
<td>每次响应后，关闭此次全部数据库链接</td>
</tr>
<tr>
<td>cache</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
<td>需要查看使用的链接是否支持长链接</td>
</tr>
<tr>
<td>filter</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>crypt</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>curl</td>
<td>是</td>
<td>否</td>
<td>可使用</td>
</tr>
<tr>
<td>request</td>
<td>否</td>
<td>是</td>
<td>可使用</td>
</tr>
<tr>
<td>response</td>
<td>否</td>
<td>是</td>
<td>可使用</td>
</tr>
<tr>
<td>cookie</td>
<td>---</td>
<td>---</td>
<td>不可使用</td>
<td>长链接下不应进行COOKIE的操作</td>
</tr>
</tbody>
</table>
<h3>(2)关于swoole扩展类库的自问自答</h3>
<ul>
<li>1、数据库使用长链接吗？ -- 不使用，每次响应后手动关闭数据链接</li>
<li>2、内存问题？-- 通过类成员方法的作用域控制内存</li>
<li>3、并发的问题？-- TODO</li>
<li>4、代码更新后如何同步？需要重启服务器吗？  -- 需要重启</li>
<li>5、PhalApi_Response::formatResult()的访问权限，是框架的调整，还是个别自我提高？ -- 扩展自我提高</li>
<li>6、注册错误回调函数？ -- 来自swoole的建议，已注册</li>
<li>7、启动和关闭、重启脚本文件？ -- 提供参考命令</li>
<li>8、一直运行？ -- TODO
<h3>(3)特别注意！</h3>
<p>由于服务在启动后，已经完成了大部分的类加载、配置读取以及PHP文件的解析，所以在对项目代码（包括配置）修改后，需要重启服务，方能生效。</p></li>
</ul>
<hr />
<h4><a href="/wikis/%5B3.8%5D-%E6%89%A9%E5%B1%95%E7%B1%BB%E5%BA%93%EF%BC%9A%E7%94%A8%E6%88%B7%E3%80%81%E4%BC%9A%E8%AF%9D%E5%92%8C%E7%AC%AC%E4%B8%89%E6%96%B9%E7%99%BB%E5%BD%95%E9%9B%86%E6%88%90.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a>#[3.11]-扩展类库：基于FastRoute的快速路由</h4>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a></h4>
<hr />
<h2>3.11.1 扩展类库：基于FastRoute的快速路由</h2>
<p>此扩展基于 <a href="https://github.com/nikic/FastRoute">FastRoute</a> 实现，需要 <strong>PHP 5.4.0</strong> 及以上版本，可以通过配置实现自定义路由配置，从而轻松映射到PhalApi中的service接口服务。  </p>
<h2>3.11.2 安装和配置</h2>
<h3>(1)扩展包下载</h3>
<p>从  <a href="http://git.oschina.net/dogstar/PhalApi-Library">PhalApi-Library</a>  扩展库中下载获取 <strong>FastRoute</strong> 扩展包，如使用：</p>
<pre><code class="language-javascript">git clone https://git.oschina.net/dogstar/PhalApi-Library.git</code></pre>
<p>然后把 <strong>FastRoute</strong> 目录复制到 <strong>./PhalApi/Library/</strong> 下，即：</p>
<pre><code class="language-javascript">cp ./PhalApi-Library/FastRoute/ ./PhalApi/Library/ -R</code></pre>
<p>到此安装完毕！接下是插件的配置。</p>
<h3>(2)扩展包配置</h3>
<p>我们需要在 <strong>./Config/app.php</strong> 配置文件中追加以下配置：</p>
<pre><code class="language-javascript">    /**
     * 扩展类库 - 快速路由配置
     */
    'FastRoute' =&gt; array(
         /**
          * 格式：array($method, $routePattern, $handler)
          *
          * @param string/array $method 允许的HTTP请求方式，可以为：GET/POST/HEAD/DELETE 等
          * @param string $routePattern 路由的正则表达式
          * @param string $handler 对应PhalApi中接口服务名称，即：?service=$handler
          */
        'routes' =&gt; array(
            array('GET', '/user/get_base_info/{user_id:\d+}', 'User.GetBaseInfo'),
            array('GET', '/user/get_multi_base_info/{user_ids:[0-9,]+}', 'User.GetMultiBaseInfo'),
        ),
    ),
</code></pre>
<h3>(3)nginx的协助配置（省略index.php）</h3>
<p>如果是使用nginx的情况下，需要添加以下配置：</p>
<pre><code class="language-javascript">    if (-f $request_filename) {
        expires max;
        break;
    }

    if (!-e $request_filename) {
        rewrite ^/(.*)$ /index.php/$1 last;
    }</code></pre>
<p>然后重启nginx。  </p>
<h2>3.11.3 入门使用</h2>
<h3>(1)入口注册</h3>
<pre><code class="language-javascript">//$ vim ./Public/index.php

$loader-&gt;addDirs('Library');

// 其他代码....

//显式初始化，并调用分发
DI()-&gt;fastRoute = new FastRoute_Lite();
DI()-&gt;fastRoute-&gt;dispatch();

/** ---------------- 响应接口请求 ---------------- **/

$api = new PhalApi();
$rs = $api-&gt;response();
$rs-&gt;output();</code></pre>
<h2>3.11.3 调用效果及扩展</h2>
<h3>(1)通过新的路由正常访问</h3>
<p>在完成上面的配置后，我们就可以这样进行页面访问测试：</p>
<pre><code>  http://library.phalapi.com/user/get_base_info/1
  等效于：http://library.phalapi.com/?service=User.GetBaseInfo&amp;user_id=1

  http://library.phalapi.com/user/get_multi_base_info/1,2
  等效于：http://library.phalapi.com/?service=User.GetMultiBaseInfo&amp;user_ids=1,2</code></pre>
<h3>(2)非法访问</h3>
<p>当请求的HTTP方法与配置的不符合时，就会返回405错误，如我们配置了：</p>
<pre><code class="language-javascript">array('POST', '/user/{id:\d+}/{name}', 'handler2'),</code></pre>
<p>但是通过GET方式来访问，即：</p>
<pre><code class="language-javascript">http://library.phalapi.com/user/123/name</code></pre>
<p>则会返回：</p>
<pre><code class="language-javascript">{
    "ret": 405,
    "data": [],
    "msg": "快速路由的HTTP请求方法错误，应该为：POST"
}</code></pre>
<h3>(3)路由配置错误</h3>
<p>当在./Config/app.php的文件里配置错误的路由时，会直接抛出FastRoute\BadRouteException异常，以及时提示开发人员修正。</p>
<h3>(4)异常错误处理器</h3>
<p>我们也可以实现FastRoute_Handler接口来自定义我们自己的错误异常处理回调函数。如：</p>
<pre><code class="language-javascript">class FastRoute_Handler_App implements FastRoute_Handler {

    public function excute(PhalApi_Response $response) {
        // ... ...
    }
}</code></pre>
<p>然后，在分发时指定handler：</p>
<pre><code class="language-javascript">DI()-&gt;fastRoute-&gt;dispatch(new FastRoute_Handler_App());</code></pre>
<h3>3.11.4 更多路由配置说明</h3>
<p>请访问 <a href="https://github.com/nikic/FastRoute">FastRoute</a> ，查看其官方说明。</p>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a>#[4.2]-开发实战2：模拟优酷开放平台接口项目开发</h4>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B4.3%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%983%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E5%B0%8F%E5%9E%8B%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%EF%BC%88%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%E6%8A%95%E7%A5%A8%E6%B4%BB%E5%8A%A8%EF%BC%89.html">下一章</a></h4>
<hr />
<h2>4.2.1 模拟的业务场景</h2>
<p>在这个开发实战中，我们将模拟实现优酷的开放平台接口，即：<a href="http://open.youku.com/docs"><a href="http://open.youku.com/docs">http://open.youku.com/docs</a></a>  </p>
<p>但这里不涉及具体的内部开发（我们也确实不得而知），而是只从外部的角度，通过PhalApi框架搭建类似的平台接口架构。  </p>
<h2>4.2.2 优酷URL分析与路由Rewrite</h2>
<p>以下是优酷平台的部分接口URL：</p>
<pre><code class="language-javascript">#单条视频基本信息(videos/show_basic)
https://openapi.youku.com/v2/videos/show_basic.json

#我的详细信息(users/myinfo) 
https://openapi.youku.com/v2/users/myinfo.json

#评论创建(comments/create)
https://openapi.youku.com/v2/comments/create.json

#搜索节目通过关键词(searches/show/by_keyword)
https://openapi.youku.com/v2/searches/show/by_keyword.json
</code></pre>
<p>从上面的接口URL，我们可以明显发现一些规律。即：  </p>
<pre><code>URL =  接口域名 + 版本 + 相对路径.json</code></pre>
<h3>与PhalApi接口规则的冲突</h3>
<p>但在PhalApi框架中，我们是通过&amp;service=XXX参数来指定需要的服务的。为此，我们需要在服务端配置一些Rewrite规则以支持这些URL。
简单地，我们可以这样在nginx配置：</p>
<pre><code class="language-javascript">    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }
</code></pre>
<p>并为了更兼容PhalApi的风格，我们在入口文件将接收到的service首字母强制为大写，即：  </p>
<pre><code class="language-javascript">//$ vim ./Public/v2/index.php

if (isset($_REQUEST['service'])) {
    $_REQUEST['service'] = ucwords($_REQUEST['service']);
}</code></pre>
<p>重启一下nginx后，我们可以有浏览器，试着访问：</p>
<pre><code>https://openapi.youku.com/v2/videos/show_basic.json</code></pre>
<p>我们将会看到：  </p>
<pre><code class="language-javascript">{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：接口服务Videos.show_basic不存在"
}</code></pre>
<p>即表明Rewrite规则已生效，并能正常工作了，哈哈！  </p>
<p>以下是更完整的nginx配置：</p>
<pre><code class="language-javascript">server {
    root /path/to/openapi.youku.com/Public;
    index index.html index.htm index.php;

    error_log /var/log/nginx/.error_log;
    access_log /var/log/nginx/openapi.youku.com.access_log;

    server_name openapi.youku.com;

    location / {
        try_files $uri $uri/ /index.html;
    }

    if ( !-f $request_filename )
    {
        rewrite ^/v2/(.*)/(.*).json /v2/?service=$1.$2;
    }

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;

        fastcgi_pass 127.0.0.1:9000;
        include fastcgi_params;
    }

}</code></pre>
<h2>4.2.3 项目的主要目录结构</h2>
<p>首先，我们建立了一个Youku的项目目录，并按不同的版本划分了不同的模块，如：  </p>
<pre><code class="language-javascript">$ tree ./Youku/
./Youku/
└── V2
    ├── Api
    │   └── Default.php
    ├── Domain
    ├── Model
    └── Tests
        ├── Api
        ├── Domain
        ├── Model
        ├── phpunit_user_getbaseinfo.xml
        ├── phpunit.xml
        └── test_env.php

8 directories, 4 files
</code></pre>
<p>然后，为不同的版本，提供不同的入口，如：  </p>
<pre><code class="language-javascript">$ tree ./Public/
./Public/
├── checkApiParams.php
├── index.php
├── init.php
└── v2
    ├── checkApiParams.php
    ├── index.php
    └── listAllApis.php

1 directory, 6 files
</code></pre>
<h2>4.2.4 简单的模拟实现</h2>
<p>现在，到了接口具体开发的环节，我们将模拟开发 <a href="http://open.youku.com/docs/docs?id=44">单条视频基本信息(videos/show_basic)</a>  </p>
<p>首先，我们可以定义接口：  </p>
<pre><code class="language-javascript">//$ vim ./Youku/V2/Api/Videos.php

&lt;?php

class Api_Videos extends PhalApi_Api {

    public function getRules() {
        return array(
            'show_basic' =&gt; array(
                'clientId' =&gt; array('name' =&gt; 'client_id', 'require' =&gt; true, 'desc' =&gt; '应用Key'),
                'videoId' =&gt; array('name' =&gt; 'video_id', 'desc' =&gt; '视频ID'),
                'videoUrl' =&gt; array('name' =&gt; 'video_url', 'desc' =&gt; '视频播放页URL'),
            ),
        );
    }

    /**
     * 单条视频基本信息(videos/show_basic)
     * 
     * @return string id 视频唯一ID 
     * @return string title 视频标题
     * @return string link 视频播放链接
     * @return string thumbnail 视频截图
     * @return int duration 视频时长，单位：秒
     * @return ... ...
     */
    public function show_basic() {

    }
}
</code></pre>
<p>然后，使用浏览器在线访问接口文档，访问：  </p>
<pre><code>http://openapi.youku.com/v2/checkApiParams.php?service=Videos.show_basic</code></pre>
<p>可以看到：<br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/20150809.png" alt="a pic" /><br />
可以看出，这与优酷平台上的接口文档是非常相近的。  </p>
<p>最后，我们可以简单模拟实现：  </p>
<pre><code class="language-javascript">    public function show_basic() {
        $rs = '{
            "id" : "XMjg1MTcyNDQ0",
                "title" : "泡芙小姐的灯泡 11",
                "link" : "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
                "thumbnail" : "http://g4.ykimg.com/0100641F464E1FC...",
                "duration" : "910",
                "category" : "原创",
                "state" : "normal",
                "published" : "2011-07-15 09:00:42",
                "description" : "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
                "player" : "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
                "public_type" : "all",
                "copyright_type" : "original",
                "user" :
                {
                    "id" : 58921428,
                    "name" : "泡芙小姐",
                    "link" : "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
                },
                "operation_limit": ["COMMENT_DISABLED"],
                "streamtypes" : ["flv","flvhd","hd"]
        }';

        return json_decode($rs, true);
    }
</code></pre>
<h2>4.2.5 接口调用效果</h2>
<p>虽然是模拟返回（其实是直接强制返回优酷开放平台上的示例数据），但我们还是可以来看下在模拟实现后的接口调用效果。  </p>
<p>首先，是缺少client_id时的非法请求：  </p>
<pre><code class="language-javascript">#请求
http://openapi.youku.com/v2/videos/show_basic.json

#返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：缺少必要参数client_id"
}</code></pre>
<p>然后，尝试一个合法的请求：</p>
<pre><code class="language-javascript">#请求
http://openapi.youku.com/v2/videos/show_basic.json?client_id=test

#返回
{
    "ret": 200,
    "data": {
        "id": "XMjg1MTcyNDQ0",
        "title": "泡芙小姐的灯泡 11",
        "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
        "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
        "duration": "910",
        "category": "原创",
        "state": "normal",
        "published": "2011-07-15 09:00:42",
        "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
        "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
        "public_type": "all",
        "copyright_type": "original",
        "user": {
            "id": 58921428,
            "name": "泡芙小姐",
            "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
        },
        "operation_limit": [
            "COMMENT_DISABLED"
        ],
        "streamtypes": [
            "flv",
            "flvhd",
            "hd"
        ]
    },
    "msg": ""
}</code></pre>
<p>很好，目前运行效果相当流畅。<br />
虽然如此，但我们明显看到了问题所在。  </p>
<h2>4.2.6 返回格式的自行调整</h2>
<p>在上一节中，我们很明显看到了返回格式与优酷现有的不一样，因为PhalApi框架多了一层。<br />
其实，这些调整对于不同的项目来说，都是非常简单。    </p>
<p>当项目需要返回的格式有定制化需求时，可以先自实现response服务，再注册。<br />
在此场景，即：  </p>
<h3>先自定义response服务</h3>
<p>我们先创建一个公共的目录./Youku/Common，再创建项目需要的特定响应类：  </p>
<pre><code class="language-javascript">//$ vim ./Youku/Common/Response.php

&lt;?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        return $this-&gt;data;
    }
}
</code></pre>
<h3>注册response服务</h3>
<p>接着，我们对response进行注册：  </p>
<pre><code class="language-javascript">//$ vim ./Public/v2/index.php

//装载你的接口
DI()-&gt;loader-&gt;addDirs(array('Youku', 'Youku/V2'));

DI()-&gt;response = 'Common_Response';</code></pre>
<p>这里需要稍微注意一下，我们要在装载Youku目录后，才能注册DI()-&gt;response。  </p>
<h3>再次返回</h3>
<p>回到刚才那个请求链接，我们可以发现，当再次调用时，会返回：  </p>
<pre><code class="language-javascript">{
    "id": "XMjg1MTcyNDQ0",
    "title": "泡芙小姐的灯泡 11",
    "link": "http://v.youku.com/v_show/id_XMjg1MTcyNDQ0.html",
    "thumbnail": "http://g4.ykimg.com/0100641F464E1FC...",
    "duration": "910",
    "category": "原创",
    "state": "normal",
    "published": "2011-07-15 09:00:42",
    "description": "当一个人在一座城市搬11次家。就意味着准备在这个城市买房了。",
    "player": "http://player.youku.com/player.php/sid/XMjg1MTcyNDQ0/v.swf",
    "public_type": "all",
    "copyright_type": "original",
    "user": {
        "id": 58921428,
        "name": "泡芙小姐",
        "link": "http://u.youku.com/user_show/id_UMjM1Njg1NzEy.html"
    },
    "operation_limit": [
        "COMMENT_DISABLED"
    ],
    "streamtypes": [
        "flv",
        "flvhd",
        "hd"
    ]
}</code></pre>
<h2>4.2.7 签名验证</h2>
<p>以上我们调整了返回格式，这使得我们的项目开发，越来越达到优酷开放平台接口的标准了（当然，是假设）。  </p>
<p>但有一点，我们是不能忽视的，在很多项目中同样是不能忽视的。那就是：接口的签名验证。  </p>
<p>我们可以先来看下优酷开放平台是怎么处理接口签名的。  </p>
<p>简单地，优酷会为每一个接入方提供一个client_id，然后在每次接口请求时，通过都需要传递此参数。  </p>
<p>为此，我们针对这个client_id编写一个简单的客户端验证服务。如：  </p>
<pre><code class="language-javascript">//$ vim ./Youku/Common/ClientCheck.php

&lt;?php

class Common_ClientCheck implements PhalApi_Filter {

    public function check() {
        $clientId = DI()-&gt;request-&gt;get('client_id');

        $allCliendIds = array(
            'phalapi',
            'oschina'
        );

        if (!in_array($clientId, $allCliendIds)) {
            throw new PhalApi_Exception_BadRequest('illegal client id');
        }
    }
}
</code></pre>
<p>然后，在入口处注册一下：</p>
<pre><code class="language-javascript">//$ vim ./Public/v2/index.php 

DI()-&gt;filter = 'Common_ClientCheck';</code></pre>
<p>当我们，再次打开刚才那个链接：</p>
<pre><code>http://openapi.youku.com/v2/videos/show_basic.json?client_id=test</code></pre>
<p>我们会看到空的返回：</p>
<pre><code class="language-javascript">[]</code></pre>
<p>这说明，对客户端的非法请求已拦截成功，但这样用户体验明显不好，因为没有任何的错误提示输出。  </p>
<p>为此，我们需要回到刚才自定义的响应类，修改一下：</p>
<pre><code class="language-javascript">//$ vim ./Youku/Common/Response.php 

&lt;?php

class Common_Response extends PhalApi_Response_Json {

    public function getResult() {
        if ($this-&gt;ret != 200) {
            return array(
                'error' =&gt; array(
                    'code' =&gt; $this-&gt;ret,
                    'type' =&gt; 'SystemException',
                    'description' =&gt; $this-&gt;msg,
                ),
            );
        }

        return $this-&gt;data;
    }
}
</code></pre>
<p>再刷新一下，可以看到和优酷平台接口近似的返回了！  </p>
<pre><code class="language-javascript">{
    "error": {
        "code": ,
        "type": "SystemException",
        "description": "非法请求：illegal client id"
    }
}</code></pre>
<p>如需要能返回数据，我们只需要传递正确的client_id（目前是固定的两个）即可：</p>
<pre><code>http://openapi.youku.com/v2/videos/show_basic.json?client_id=phalapi</code></pre>
<h2>4.2.8 尾声</h2>
<p>当然，此次的优酷接口模拟开发，我们都只是很简单地表面说明。  </p>
<p>这样的目的，不是为了让大家真的去了解优酷接口的内部实现，而是向大家展示，通过PhalApi框架，我们可以更灵活地实现各种业务需求和非功能性的需求。  </p>
<p>希望对大家有帮助，夜已深，安。  </p>
<h3>源代码请访问：</h3>
<pre><code>https://git.oschina.net/dogstar/PhalApi-Demo-Youku.git</code></pre>
<hr />
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B4.3%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%983%EF%BC%9A%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E5%B0%8F%E5%9E%8B%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%EF%BC%88%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%E6%8A%95%E7%A5%A8%E6%B4%BB%E5%8A%A8%EF%BC%89.html">下一章</a>#[4.3]-开发实战3：一个简单的小型项目开发（奔跑吧兄弟投票活动）</h4>
<h4><a href="/wikis/%5B4.2%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%982%EF%BC%9A%E6%A8%A1%E6%8B%9F%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a></h4>
<hr />
<p><em>Give it some time, everything will be okay.  -- 《那些年我们疯狂的青春/青春洋溢色彩/This Youth is Crazy》</em></p>
<h2>4.3.1 模拟的业务场景</h2>
<p>假设我们需要为“奔跑吧兄弟”综艺节目开发一套手机App的投票接口，以供用户在观看电视的同时，可以进行投票活动参与互动。  </p>
<p>下面将以此模拟的业务场景，提供一个接口开发实战的过程。  </p>
<h2>4.3.2 源代码下载</h2>
<p><a href="http://git.oschina.net/dogstar/PhalApi-Demo-Vote">PhalApi-Demo-Vote</a>  </p>
<h2>4.3.3 接口总列表</h2>
<p>以下是我们根据业务需求整理出来的接口：</p>
<ul>
<li>1、用户可以通过微信、QQ、新浪微博等渠道进行第三方登录</li>
<li>2、用户可以创建团队进行参赛，但队名不能重复</li>
<li>3、用户可以对已参赛的团队进行投票，且每个用户每天投票最多不能超过3次，支持可配置</li>
<li>4、获取已参赛团队的得票排行榜</li>
</ul>
<h2>4.3.4 主要涉及技术功能点</h2>
<ul>
<li>1、使用User扩展类库实现第三方登录操作</li>
<li>2、使用缓存存放用户每天投票的次数（为方便起见，使用文件缓存，不落地）</li>
<li>3、对接口进行签名验证（为方便起见，固定sign签名）</li>
<li>4、数据库的基本操作</li>
<li>5、自动化脚本的使用</li>
</ul>
<h2>4.3.4 快速开发流程</h2>
<p>（题外音：整个示例的开发，我个人在单元测试驱动开发下，只用了两个多小时，其中还包括对模板场景的业务构思、建表、编写单元测试代码等）。  </p>
<h3>(1)创建项目和部署环境</h3>
<p>把PhalApi最新的框架代码下载后，并将User扩展类库按文档说明配置后，将项目部署到了以下接口域名：</p>
<pre><code>http://api.vote.phalapi.com</code></pre>
<p>测试一下：</p>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=phalapi

//返回
{
    "ret": 200,
    "data": {
        "title": "Hello World!",
        "content": "PHPer您好，欢迎使用PhalApi！",
        "version": "1.1.4",
        "time": 1431796924
    },
    "msg": ""
}</code></pre>
<p>Good! 下面是简明的开发过程。</p>
<h3>(2)单元测试驱动开发</h3>
<p>在定好接口后：</p>
<pre><code class="language-javascript">&lt;?php
class Api_Act extends PhalApi_Api {

    public function joinIn() {
    }

    public function showList() {
    }

    public function vote() {
    }
}</code></pre>
<p>便可使用脚本，快速生成单元测试的骨架代码：</p>
<pre><code class="language-javascript">$ cd ./Vote/Tests/Api
$ phalapi_buildtests ../../Api/Act.php Api_Act ../test_env.php  &gt; Api_Act_Test.php</code></pre>
<h3>(3)快速开发</h3>
<p>开发过程此处略，但在单元测试驱动的引导下，很快就产出了以下高质量的代码：</p>
<pre><code class="language-javascript">.
├── Api
│   ├── Act.php
│   └── Default.php
├── Common
│   └── SignFilter.php
├── Domain
│   ├── Team.php
│   └── Vote.php
├── Model
│   ├── Team.php
│   ├── UserVoteRecord.php
│   └── Vote.php
└── Tests
    ├── Api
    │   ├── Api_Act_Test.php
    │   └── Api_Default_Test.php
    ├── Domain
    ├── Model
    ├── phpunit.xml
    └── test_env.php
</code></pre>
<h3>(4)单元测试全部通过了！</h3>
<p>为了方便大家查看，已省略了部分的调试内容，但保留了测试过程中全部执行的SQL语句，如下：</p>
<pre><code class="language-javascript">$ phpunit ./Api_Act_Test.php 
PHPUnit 4.3.4 by Sebastian Bergmann.

[1 - 0.06911s]DELETE FROM phalapi_team WHERE (team_name = 'test team name');&lt;br&gt;
[2 - 0.06487s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[3 - 0.06553s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');&lt;br&gt;
[4 - 0.0653s]INSERT INTO phalapi_team (team_name) VALUES ('test team name');&lt;br&gt;
[5 - 0.0699s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[6 - 0.06555s]SELECT COUNT(id) FROM phalapi_team WHERE (team_name = 'test team name');&lt;br&gt;
[9 - 0.06778s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[10 - 0.06603s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);&lt;br&gt;
[11 - 0.06825s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;&lt;br&gt;
[12 - 0.07374s]UPDATE phalapi_vote SET team_id = 3, vote_num = 22 WHERE (team_id = 3);&lt;br&gt;
[13 - 0.07012s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[14 - 0.06682s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);&lt;br&gt;
[15 - 0.07433s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;&lt;br&gt;
[16 - 0.07283s]UPDATE phalapi_vote SET team_id = 3, vote_num = 23 WHERE (team_id = 3);&lt;br&gt;
[17 - 0.07307s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[18 - 0.07501s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);&lt;br&gt;
[19 - 0.07135s]SELECT vote_num FROM phalapi_vote WHERE (team_id = 3) LIMIT 1;&lt;br&gt;
[20 - 0.07653s]UPDATE phalapi_vote SET team_id = 3, vote_num = 24 WHERE (team_id = 3);&lt;br&gt;
[21 - 0.07215s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[22 - 0.06722s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 3);&lt;br&gt;
[23 - 0.06506s]SELECT expires_time FROM phalapi_user_session_1 WHERE (user_id = '1') AND (token = '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731');&lt;br&gt;
[24 - 0.06732s]SELECT COUNT(id) FROM phalapi_team WHERE (id = 404);&lt;br&gt;

Time: 1.97 seconds, Memory: 7.00Mb

OK (4 tests, 42 assertions)</code></pre>
<h3>(5)运行效果 - Part 1</h3>
<p>在我们通过第三方登录后，我们就可以这样进行接口操作了。  </p>
<p>首先，让我们添加两个参赛团队：</p>
<pre><code class="language-javascript">//奔跑吧兄弟（蓝队）
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.JoinIn&amp;team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E8%93%9D%E9%98%9F%EF%BC%89&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "5"
    },
    "msg": ""
}

//奔跑吧兄弟（红队）
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.JoinIn&amp;team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "team_id": "6"
    },
    "msg": ""
}</code></pre>
<p>然后，让我们进行疯狂地投票：</p>
<pre><code class="language-javascript">//第一次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=5&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":1},"msg":""}

//第二次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=5&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":2},"msg":""}

//第三次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=5&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{"ret":200,"data":{"code":0,"vote_num":3},"msg":""}

//第四次投票
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=5&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回 - 注意此返回！
{"ret":200,"data":{"code":2,"vote_num":0},"msg":""}</code></pre>
<p>最后，看一下排行榜：</p>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.ShowList&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 0,
        "teams": [
            {
                "id": 5,
                "team_name": "奔跑吧兄弟（蓝队）",
                "vote_num": 3
            },
            {
                "id": 6,
                "team_name": "奔跑吧兄弟（红队）",
                "vote_num": 0
            }
        ]
    },
    "msg": ""
}</code></pre>
<p>至此，我们已经可以把接口交付给客户端同学使用啦！<br />
当然，我们还需要稍微整理输出WIKI文档~~~<br />
部分接口返回的结果可能与你实际看到的不一样，因为数据会在变化而且有单元测试的测试数据。  </p>
<h3>(6)运行效果 - Part 2</h3>
<p>接口服务，不仅仅需要提供正常的业务功能，还需要考虑到各种客户端使用的情况，包括非法的请求，或者不合的调用，比如防刷票。  </p>
<p>这一部分，主要展示接口在各种异常情况下的响应能力。  </p>
<h4>签名失败</h4>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=XXX&amp;service=Act.JoinIn&amp;team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 400,
    "data": [
    ],
    "msg": "非法请求：wrong sign"
}</code></pre>
<h4>无登录态</h4>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.JoinIn&amp;team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&amp;token=XXX&amp;user_id=1
//返回
{
    "ret": 401,
    "data": [
    ],
    "msg": "非法请求：user need to login again"
}</code></pre>
<h4>重复参赛</h4>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.JoinIn&amp;team_name=%E5%A5%94%E8%B7%91%E5%90%A7%E5%85%84%E5%BC%9F%EF%BC%88%E7%BA%A2%E9%98%9F%EF%BC%89&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "team_id": 0
    },
    "msg": ""
}</code></pre>
<h4>当天投票次数已达最大</h4>
<pre><code class="language-javascript">//第四次投票后
http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=5&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回 - 注意此返回！
{
    "ret": 200,
    "data": {
        "code": 2,
        "vote_num": 0
    },
    "msg": ""
}</code></pre>
<h4>投票的团队不存在</h4>
<pre><code class="language-javascript">http://api.vote.phalapi.com/vote/?sign=phalapi&amp;service=Act.Vote&amp;team_id=404&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731&amp;user_id=1
//返回
{
    "ret": 200,
    "data": {
        "code": 1,
        "vote_num": 0
    },
    "msg": ""
}</code></pre>
<p>在进行上面的非法请求后，我们可以同时关注后台的日记：</p>
<pre><code class="language-javascript">$ tailf ./Runtime/log/201505/20150517.log 
2015-05-17 01:35:32|DEBUG|user not login|{"userId":null,"token":null}
2015-05-17 01:36:01|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:37:52|DEBUG|user can not vote today|{"userId":1,"teamId":5}
2015-05-17 01:45:25|DEBUG|user need to login again|{"expiresTime":0,"userId":"1","token":"XXX"}
2015-05-17 01:48:13|DEBUG|user can not vote today|{"userId":1,"teamId":5}</code></pre>
<h3>(7)需要的数据库表</h3>
<p>以下为关键的表，其他表，可以通过脚本自动生成，然后导入。</p>
<pre><code class="language-javascript">-- ----------------------------
-- Table structure for `phalapi_team`
-- ----------------------------
DROP TABLE IF EXISTS `phalapi_team`;
CREATE TABLE `phalapi_team` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `team_name` varchar(100) DEFAULT '' COMMENT '队名',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of phalapi_team
-- ----------------------------
INSERT INTO `phalapi_team` VALUES ('3', 'egg team');
INSERT INTO `phalapi_team` VALUES ('5', '奔跑吧兄弟（蓝队）');
INSERT INTO `phalapi_team` VALUES ('6', '奔跑吧兄弟（红队）');
INSERT INTO `phalapi_team` VALUES ('17', 'test team name');

INSERT INTO `phalapi_user` VALUES ('1', 'wx_edebc877070133c65161d00799e00544', 'weixinName', '******', '4CHqOhe1Jxi3X9HmRfPOXygDnU267eCA', '1431790647', 'phpunit.png');

INSERT INTO `phalapi_user_login_weixin` VALUES ('1', 'wx_122348561111', 'ASDF', '130000000', '1');

INSERT INTO `phalapi_user_session_1` VALUES ('1', '1', '193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731', '', '1', '1431790647', '1934382647', null);</code></pre>
<h2>4.3.4 脚本的使用</h2>
<h3>(1)创建项目脚本 - phalapi_buildapp</h3>
<p>此脚本可用于快速创建项目，其使用如下：</p>
<pre><code class="language-javascript">$ ./PhalApi/phalapi-buildapp 
Usage: ./PhalApi/phalapi-buildapp &lt;app&gt;</code></pre>
<p>此脚本会根据Demo示例，快速生成一个新的项目。</p>
<h3>(2)自动生成单元测试骨架脚本 - phalapi_buildtest</h3>
<p>这是一个用来自动生成单元测试骨架的脚本，其使用如下：</p>
<pre><code class="language-javascript">$ phalapi-buildtest 

Usage: 
        php /usr/bin/phalapi-buildtest &lt;file_path&gt; &lt;class_name&gt; [bootstrap] [author = dogstar]

Demo:
        php ./build_phpunit_test_tpl.php ./Demo.php Demo &gt; Demo_Test.php</code></pre>
<p>然后，我们就可以使用辅助的PhalApiTestRunner::go()进行单元测试：</p>
<pre><code class="language-javascript">    public function testShowList()
    {
        //Step 1. 构建请求URL
        $url = 'service=Act.ShowList&amp;sign=phalapi&amp;user_id=1&amp;token=193CE82D1F4588A9A168BDE6E6B83868B1464F523D16C05206F308E51EB91731';

        //Step 2. 执行请求  
        $rs = PhalApiTestRunner::go($url);
        //var_dump($rs);

        //Step 3. 验证
        $this-&gt;assertNotEmpty($rs);
        $this-&gt;assertArrayHasKey('code', $rs);
        $this-&gt;assertArrayHasKey('teams', $rs);
        $this-&gt;assertEquals(0, $rs['code']);

        $this-&gt;assertNotEmpty($rs['teams']);
        foreach ($rs['teams'] as $team) {
            $this-&gt;assertArrayHasKey('id', $team);
            $this-&gt;assertArrayHasKey('team_name', $team);
            $this-&gt;assertArrayHasKey('vote_num', $team);

            $this-&gt;assertGreaterThanOrEqual(0, $team['vote_num']);
        }
    }</code></pre>
<h3>(3)自动生成分表建表SQL语句 - phalapi_buildsqls</h3>
<p>此脚本用于根据dbs.php配置和./Data/*.sql文件生成完整的建表语句，其使用如下：</p>
<pre><code class="language-javascript">$ ./PhalApi/phalapi-buildsqls 
Usage: ./PhalApi/phalapi-buildsqls &lt;dbs.php&gt; &lt;table&gt; [engine=InnoDB]</code></pre>
<p>假设我们需要对vote进行分表，拆分成3个表，可以这样配置：</p>
<pre><code class="language-javascript">//$ vim ./Config/dbs.php

        'vote' =&gt; array(
            'prefix' =&gt; 'phalapi_',
            'key' =&gt; 'id',
            'map' =&gt; array(
                array('start' =&gt; 0, 'end' =&gt; 2, 'db' =&gt; 'db_vote'),
            ),
        ),</code></pre>
<p>然后准备基本的建表语句：</p>
<pre><code class="language-javascript">//$vim ./Data/vote.sql

  `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',</code></pre>
<p>最后，执行生成脚本：</p>
<pre><code class="language-javascript">$ phalapi-buildsqls ./Config/dbs.php vote

/**
 * DB: phalapi_vote
 */

CREATE TABLE `phalapi_vote_0` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_1` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `phalapi_vote_2` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `team_id` bigint(20) DEFAULT '0',
  `vote_num` int(11) DEFAULT '0',
    `ext_data` text COMMENT 'json data here',
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
</code></pre>
<h2>4.3.5 在线接口参数查看</h2>
<p>可以通过以下链接，在线实时查看接口参数：</p>
<pre><code class="language-javascript">//参赛接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.JoinIn

//投票接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.Vote

//排行榜接口
http://api.vote.phalapi.com/vote/checkApiParams.php?service=Act.ShowList</code></pre>
<p>其中，投票接口的参数如下：<br />
<img src="http://webtools.qiniudn.com/20150517.jpg" alt="a pic" />  </p>
<hr />
<h4><a href="/wikis/%5B4.2%5D-%E5%BC%80%E5%8F%91%E5%AE%9E%E6%88%982%EF%BC%9A%E6%A8%A1%E6%8B%9F%E4%BC%98%E9%85%B7%E5%BC%80%E6%94%BE%E5%B9%B3%E5%8F%B0%E6%8E%A5%E5%8F%A3%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="#">下一章</a>#[6.1]-基于接口查询语言的SDK包</h4>
<h4><a href="/wikis/%E9%99%84%E5%BD%951%EF%BC%9A%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E5%8F%82%E8%80%83%E6%A8%A1%E6%9D%BF.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.2%5D-SDK%E5%8C%85%EF%BC%88JAVA%E7%89%88%EF%BC%89.html">下一章</a></h4>
<hr />
<h2>6.1.1 用一句话来描述接口请求</h2>
<p>为了给客户端统一接口请求调用的规范性、流畅性和简单易懂，我们特别为此使用了内部领域特定语言： <strong>接口查询语言</strong> （Api Structured Query Language） 。  </p>
<h3>从外部DSL的角度来看待接口查询的操作</h3>
<pre><code class="language-javascript">create

withHost host
withFilter filter
withParser parser

reset   #特别注意：重复查询时须重置请求状态

withService service
withParams paramName1 paramValue1
withParams paramName2 paramValue2
withParams ... ...
withTimeout timeout

request
</code></pre>
<p>根据此设计理念，各客户端语言都可以实现此接口请求的操作。 </p>
<h2>6.1.2 接口查询语言设计理念与示例</h2>
<h3>(1)文法：create -&gt; with -&gt; request</h3>
<p>所用到的查询文法如下（通常从上往下依次操作，顺序不强制）：  </p>
<table>
<thead>
<tr>
<th>操作</th>
<th>参数</th>
<th>是否必须</th>
<th>是否可重复调用</th>
<th>作用说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>create</td>
<td>无</td>
<td>必须</td>
<td>可以，重复调用时新建一个实例，非单例模式</td>
<td>需要先调用此操作创建一个接口实例</td>
</tr>
<tr>
<td>withHost</td>
<td>接口域名</td>
<td>必须</td>
<td>可以，重复时会覆盖</td>
<td>设置接口域名，如：<a href="http://demo.phalapi.net/">http://demo.phalapi.net/</a></td>
</tr>
<tr>
<td>withFilter</td>
<td>过滤器</td>
<td>可选</td>
<td>可以，重复时会覆盖</td>
<td>设置过滤器，与服务器的DI().filter对应，需要实现PhalApiClientFilter接口</td>
</tr>
<tr>
<td>withParser</td>
<td>解析器</td>
<td>可选</td>
<td>可以，重复时会覆盖</td>
<td>设置结果解析器，仅当不是JSON返回格式时才需要设置，需要实现PhalApiClientParser接口</td>
</tr>
<tr>
<td>reset</td>
<td>无</td>
<td>通常必须</td>
<td>可以</td>
<td>重复查询时须重置请求状态，包括接口服务名称、接口参数和超时时间</td>
</tr>
<tr>
<td>withService</td>
<td>接口服务名称</td>
<td>通常必选</td>
<td>可以，重复时会覆盖</td>
<td>设置将在调用的接口服务名称，如：Default.Index</td>
</tr>
<tr>
<td>withParams</td>
<td>接口参数名、值</td>
<td>可选</td>
<td>可以，累加参数</td>
<td>设置接口参数，此方法是唯一一个可以多次调用并累加参数的操作</td>
</tr>
<tr>
<td>withTimeout</td>
<td>超时时间</td>
<td>可选</td>
<td>可以，重复时会覆盖</td>
<td>设置超时时间，单位毫秒，默认3秒</td>
</tr>
<tr>
<td>request</td>
<td>无</td>
<td>必选</td>
<td>可以，重复发起接口请求</td>
<td>最后执行此操作，发起接口请求</td>
</tr>
</tbody>
</table>
<h3>(2)JAVA示例</h3>
<p>以JAVA版本为例，演示如何调用：<br />
最简单的调用，也就是默认接口的调用：  </p>
<pre><code class="language-javascript">PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")  //接口域名
       .request();                            //发起请求</code></pre>
<p>通常的调用，即有设置接口服务名称、接口参数和超时：</p>
<pre><code class="language-javascript">PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")          //接口服务
       .withParams("username", "dogstar")     //接口参数
       .withTimeout(3000)                     //接口超时
       .request();</code></pre>
<p>高级复杂调用，即设置了过滤器、解析器的操作：  </p>
<pre><code class="language-javascript">PhalApiClientResponse response = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withService("Default.Index")
       .withParser(new PhalApiClientParserJson()) //设置JSON解析，默认已经是此解析，这里仅作演示
       .withParams("username", "dogstar")
       .withTimeout(3000)
       .request();</code></pre>
<h2>6.1.3 更好的建议</h2>
<h3>(1)不支持面向对象的实现方式</h3>
<p>此接口查询的用法是属于基础的用法，其实现与宿主语言有强依赖关系，在不支持面向对象语言中，如javascript，可以使用函数序列的方式，如：  </p>
<pre><code class="language-javascript">create();
withHost('http://demo.phalapi.net/');
withService('Default.Index');
withParams('username', 'dogstar');
withTimeout(3000);
var rs = request();</code></pre>
<h3>(2)封装自己的接口实例</h3>
<p>通常，在一个项目里面我们只需要一个接口实例即可，但此语言没默认使用单例模式，是为了大家更好的自由度。<br />
基于此，大家在项目开发时，可以再进行封装：提供一个全局的接口查询单例，并组装基本的接口公共查询属性。  </p>
<p>即分两步：初始化接口实例，以及接口具体的查询操作。  </p>
<p>如第一步先初始化：</p>
<pre><code class="language-javascript">PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")
     .withParser(new PhalApiClientParserJson());</code></pre>
<p>第二步进行具体的接口请求：  </p>
<pre><code class="language-javascript">PhalApiClientResponse response = client.reset()  #重复查询时须重置
     .withService("Default.Index")
     .withParams("username", "dogstar")
     .withTimeout(3000)
     .request();</code></pre>
<p>这样，在其他业务场景下就不需要再重复设置这些共同的属性（如过滤器、解析器）或者共同的接口参数。</p>
<h3>(3)超时重试</h3>
<p>当接口请求超时时，统一返回 ret = 408，表示接口请求超时。此时可进行接口重试。  </p>
<p>如：  </p>
<pre><code class="language-javascript">PhalApiClient client = PhalApiClient.create()
     .withHost("http://demo.phalapi.net/")

PhalApiClientResponse response = client.request();

if (response.getRet() == 408) {
     response = client.request(); //请求重试
}</code></pre>
<hr />
<h4><a href="/wikis/%E9%99%84%E5%BD%951%EF%BC%9A%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3%E5%8F%82%E8%80%83%E6%A8%A1%E6%9D%BF.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.2%5D-SDK%E5%8C%85%EF%BC%88JAVA%E7%89%88%EF%BC%89.html">下一章</a>#[6.2]-SDK包（JAVA版）</h4>
<h4><a href="/wikis/%5B6.1%5D-%E5%9F%BA%E4%BA%8E%E6%8E%A5%E5%8F%A3%E6%9F%A5%E8%AF%A2%E8%AF%AD%E8%A8%80%E7%9A%84SDK%E5%8C%85.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.3%5D-SDK%E5%8C%85%EF%BC%88PHP%E7%89%88%EF%BC%89.html">下一章</a></h4>
<hr />
<h2>6.2.1 使用说明</h2>
<p>将框架目录下的 ./SDK/JAVA/net 目录中的全部代码拷贝到项目里面即可使用。如：<br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/qq20151017100539.jpg" alt="" />  </p>
<h2>6.2.2 代码示例</h2>
<p>如下是使用的代码场景片段。  </p>
<p>首先，我们需要导入SDK包：</p>
<pre><code class="language-javascript">//FullscreenActivity.java
import net.phalapi.sdk.*;</code></pre>
<p>然后，准备一个子线程调用，并在此线程中实现接口请求：</p>
<pre><code class="language-javascript">    /**
     * 网络操作相关的子线程
     */  
    Runnable networkTask = new Runnable() {  

        @Override  
        public void run() {  
            // TODO  
            // 在这里进行 http request.网络请求相关操作  

            PhalApiClient client = PhalApiClient.create()
                        .withHost("http://demo.phalapi.net/");

            PhalApiClientResponse response = client
                        .withService("Default.Index")
                        .withParams("username", "dogstar")
                        .withTimeout(3000)
                        .request();

            String content = "";
            content += "ret=" + response.getRet() + "\n";
            if (response.getRet() == 200) {
                try {
                    JSONObject data = new JSONObject(response.getData());
                    content += "data.title=" + data.getString("title") + "\n";
                    content += "data.content=" + data.getString("content") + "\n";
                    content += "data.version=" + data.getString("version") + "\n";
                } catch (JSONException ex) {

                }
            }
            content += "msg=" + response.getMsg() + "\n";

            Log.v("[PhalApiClientResponse]", content);

            Message msg = new Message();  
            Bundle data = new Bundle();  
            data.putString("value", content);  
            msg.setData(data);  
            handler.sendMessage(msg);  
        }  
    }; </code></pre>
<p>接着，实现线程回调的hander：</p>
<pre><code class="language-javascript">    Handler handler = new Handler() {  
        @Override  
        public void handleMessage(Message msg) {  
            super.handleMessage(msg);  
            Bundle data = msg.getData();  
            String val = data.getString("value");  
            Log.i("mylog", "请求结果为--&gt;" + val);  
            // TODO  
            // UI界面的更新等相关操作  
        }  
    }; </code></pre>
<p>最后，在我们需要的地方启动：</p>
<pre><code class="language-javascript">    View.OnClickListener mDummyBtnClickListener = new View.OnClickListener() {

        @Override
        public void onClick(View arg0) {
            // 开启一个子线程，进行网络操作，等待有返回结果，使用handler通知UI  
            new Thread(networkTask).start();  

            // ....
        }
    };</code></pre>
<h3>再一次调用和异常请求</h3>
<p>当我们需要再次使用同一个接口实例进行请求时，需要先进行重置，以便清空之前的接口参数，如：</p>
<pre><code class="language-javascript">//再一次请求
response = client.reset() //重置
        .withService("User.GetBaseInfo")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
if (response.getRet() == 200) {
    try {
        JSONObject data = new JSONObject(response.getData());
        JSONObject info = new JSONObject(data.getString("info"));

        content += "data.info.id=" + info.getString("id") + "\n";
        content += "data.info.name=" + info.getString("name") + "\n";
        content += "data.info.from=" + info.getString("from") + "\n";
    } catch (JSONException ex) {

    }
}
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);</code></pre>
<p>异常情况下，即ret != 200时，将返回错误的信息，如：</p>
<pre><code class="language-javascript">//再来试一下异常的请求
response = client.reset()
        .withService("XXX.XXXX")
        .withParams("user_id", "1")
        .request();

content = "";
content += "ret=" + response.getRet() + "\n";
content += "msg=" + response.getMsg() + "\n";

Log.v("[PhalApiClientResponse]", content);</code></pre>
<h2>6.2.3 运行效果</h2>
<p>运行后，查询log，可以看到：<br />
<img src="http://7qnay5.com1.z0.glb.clouddn.com/QQ20151017154114.jpg" alt="" />  </p>
<p>可以注意到，在调试模式时，会有接口请求的链接和返回的结果日记，如：  </p>
<pre><code>10-17 07:40:55.268: D/[PhalApiClient requestUrl](1376): http://demo.phalapi.net/?service=User.GetBaseInfo&amp;user_id=1
10-17 07:40:55.364: D/[PhalApiClient apiResult](1376): {"ret":200,"data":{"code":0,"msg":"","info":{"id":"1","name":"dogstar","from":"oschina"}},"msg":""}</code></pre>
<h2>6.2.4 扩展你的过滤器和结果解析器</h2>
<h3>(1)扩展过滤器</h3>
<p>当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。  </p>
<p>当需要扩展时，分两步。首先，需要实现过滤器接口：  </p>
<pre><code class="language-javascript">class MyFilter implements PhalApiClientFilter {

        public void filter(String service, Map&lt;String, String&gt; params) {
            //TODO ...
        }
}</code></pre>
<p>然后设置过滤器：</p>
<pre><code class="language-javascript">PhalApiClientResponse response = PhalApiClient.create()
           .withHost("http://demo.phalapi.net/")
           .withFilter(new MyFilter())
           // ...
           .request();</code></pre>
<h3>(2)扩展结果解析器</h3>
<p>当返回的接口结果不是JSON格式时，可以重新实现此接口。  </p>
<p>当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。</p>
<h2>6.2.5 特别注意：Android之NetworkOnMainThreadException异常</h2>
<p>由于此SDK包是使用HttpURLConnection发起请求时，所以在主线程调用时会触发NetworkOnMainThreadException异常，具体可参考： <a href="http://blog.csdn.net/mad1989/article/details/25964495">Android之NetworkOnMainThreadException异常</a>  </p>
<p>所以，需要使用子线程来发起请求，或者重新继承改用异步的请求。  </p>
<hr />
<h4><a href="/wikis/%5B6.1%5D-%E5%9F%BA%E4%BA%8E%E6%8E%A5%E5%8F%A3%E6%9F%A5%E8%AF%A2%E8%AF%AD%E8%A8%80%E7%9A%84SDK%E5%8C%85.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.3%5D-SDK%E5%8C%85%EF%BC%88PHP%E7%89%88%EF%BC%89.html">下一章</a>#[6.3]-SDK包（PHP版）</h4>
<h4><a href="/wikis/%5B6.2%5D-SDK%E5%8C%85%EF%BC%88JAVA%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.4%5D-SDK%E5%8C%85%EF%BC%88object-c%E7%89%88%EF%BC%89.html">下一章</a></h4>
<hr />
<h2>6.3.1 使用说明</h2>
<p>将框架目录下的 ./SDK/PHP/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。  </p>
<h2>6.3.2 代码示例</h2>
<p>如下是使用的代码场景片段。  </p>
<p>首先，我们需要导入SDK包：</p>
<pre><code class="language-javascript">&lt;?php

require_once dirname(__FILE__) . '/PhalApiClient.php';

$client = PhalApiClient::create()
        -&gt;withHost('http://demo.phalapi.net/');

$rs = $client-&gt;reset()
    -&gt;withService('Default.Index')
    -&gt;withParams('username', 'dogstar')
    -&gt;withTimeout(3000)
    -&gt;request();

var_dump($rs-&gt;getRet());
echo "\n";
var_dump($rs-&gt;getData());
echo "\n";
var_dump($rs-&gt;getMsg());</code></pre>
<h2>6.3.3 运行效果</h2>
<p>运行后，可以看到：  </p>
<pre><code class="language-javascript">int(200)

array(4) {
    ["title"]=&gt;
    string(12) "Hello World!"
    ["content"]=&gt;
    string(36) "dogstar您好，欢迎使用PhalApi！"
    ["version"]=&gt;
    string(5) "1.2.1"
    ["time"]=&gt;
    int(1444925238)
}

string(0) ""</code></pre>
<h2>6.3.4 更多调用</h2>
<p>当需要重复调用时，需要先进行 <strong>重置操作</strong> ，如：</p>
<pre><code class="language-javascript">//one more time
$rs = $client-&gt;reset()
    -&gt;withService("User.GetBaseInfo")
    -&gt;withParams("user_id", "1")
    -&gt;request();

var_dump($rs-&gt;getRet());
echo "\n";
var_dump($rs-&gt;getData());
echo "\n";
var_dump($rs-&gt;getMsg());</code></pre>
<p>当请求有异常时，返回的 ret!= 200，如：</p>
<pre><code class="language-javascript">//illegal request
$rs = $client-&gt;reset()
    -&gt;withService("XXX.XXXXX")
    -&gt;withParams("user_id", "1")
    -&gt;request();

var_dump($rs-&gt;getRet());
echo "\n";
var_dump($rs-&gt;getData());
echo "\n";
var_dump($rs-&gt;getMsg());</code></pre>
<p>以上的输出为： </p>
<pre><code class="language-javascript">--------------------
int(200)

array(3) {
  ["code"]=&gt;
  int(0)
  ["msg"]=&gt;
  string(0) ""
  ["info"]=&gt;
  array(3) {
    ["id"]=&gt;
    string(1) "1"
    ["name"]=&gt;
    string(7) "dogstar"
    ["from"]=&gt;
    string(7) "oschina"
  }
}

string(0) ""

--------------------
int(400)

array(0) {
}

string(45) "非法请求：接口服务XXX.XXXXX不存在"
</code></pre>
<h2>6.3.5 扩展你的过滤器和结果解析器</h2>
<h3>(1)扩展过滤器</h3>
<p>当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。  </p>
<p>当需要扩展时，分两步。首先，需要实现过滤器接口：  </p>
<pre><code class="language-javascript">&lt;?php

class MyFilter implements PhalApiClientFilter {

        public function filter($service, array &amp;$params) {
            //TODO ...
        }
}</code></pre>
<p>然后设置过滤器：</p>
<pre><code class="language-javascript">&lt;?php

$rs = PhalApiClient.create()
       .withHost("http://demo.phalapi.net/")
       .withFilter(new MyFilter())
       // ...
       .request();</code></pre>
<h3>(2)扩展结果解析器</h3>
<p>当返回的接口结果不是JSON格式时，可以重新实现此接口。  </p>
<p>当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。</p>
<hr />
<h4><a href="/wikis/%5B6.2%5D-SDK%E5%8C%85%EF%BC%88JAVA%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.4%5D-SDK%E5%8C%85%EF%BC%88object-c%E7%89%88%EF%BC%89.html">下一章</a>#[6.4]-SDK包（Objective-C版）</h4>
<h4><a href="/wikis/%5B6.3%5D-SDK%E5%8C%85%EF%BC%88PHP%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/#">下一章</a></h4>
<hr />
<pre><code>该文档由 @Aevit 提供</code></pre>
<h2>6.4.1 使用说明</h2>
<p>将框架目录下的 ./SDK/Objective-C/ 目录中的全部代码拷贝到项目里面即可使用。如：<br />
<img src="http://aevit.qiniudn.com/PhalApiClient-SDK-files.jpeg" alt="" />  </p>
<pre><code>请求方式，可以使用系统的NSURLSession，或第三方的网络请求库等
这里我们使用第三方的AFNetworking，进行二次封装（https://github.com/AFNetworking/AFNetworking）
如需使用其他网络请求方式（如官方的NSURLSession），请继承自PhalApiClient，重写request方法即可，可参考AFNPhalApiClient</code></pre>
<h2>6.4.2 代码示例</h2>
<p>如下是使用的代码场景片段。  </p>
<p>首先，我们需要导入SDK包：</p>
<pre><code class="language-Objective-C">#import "AFNPhalApiClient.h"</code></pre>
<p>然后，准备按以下方法实现接口请求即可：  </p>
<pre><code class="language-Objective-C">// 待POST的参数
NSDictionary *params = @{@"demo_key_1": @"your_key", @"demo_key_2": @"1.0"};

// 使用AFNPhalApiClient
[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params] requestWithFormDataBlock:^(id formData) {
    // 如需上传文件（图片等），请参照如下格式
    [formData appendPartWithFileData:UIImageJPEGRepresentation([UIImage imageNamed:@"head.JPG"], 1) name:@"file" fileName:@"image.jpg" mimeType:@"image/jpeg"];
} completeBlock:^(id resultObject) {
    PALog(@"resultObject: %@", resultObject);
} failureBlock:^(NSError *error) {
    PALog(@"error: %@", error);
}];

// 打印url查看
PALog(@"total url: %@", [[AFNPhalApiClient sharedClient] printTotalUrlStr]);</code></pre>
<h2>6.4.3 运行效果</h2>
<p>运行后，查询log，可以看到：<br />
<img src="http://aevit.qiniudn.com/PhalApiClient-SDK-demo-result.jpg" alt="" />  </p>
<p>可以注意到，调用完成后，会有接口请求的链接和返回的结果，如：  </p>
<pre><code>2015-10-19 18:28:01.503 PhalApiClientDemo[23161:1199740] total url: http://api1.aevit.xyz/?service=Default.Index&amp;amp;demo_key_2=1.0&amp;amp;demo_key_1=your_key

2015-10-19 18:28:01.536 PhalApiClientDemo[23161:1199740] resultObject: {
    data =     {
        content = "PHPer\U60a8\U597d\Uff0c\U6b22\U8fce\U4f7f\U7528PhalApi\Uff01";
        time = 1445250481;
        title = "Hello World!";
        version = "1.2.1";
    };
    msg = "";
    ret = 200;
}</code></pre>
<h2>6.4.4 扩展你的过滤器和结果解析器</h2>
<h3>(1)扩展过滤器</h3>
<p>当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。  </p>
<p>当需要扩展时，分两步。首先，需要实现过滤器接口：  </p>
<pre><code class="language-Objective-C">
@interface MyFilter : PhalApiClientFilter
@end

@implementation PhalApiClientFilter
/**
 *  接口过滤器
 *  可用于接口签名生成
 *
 *  @param service 接口服务名称
 *  @param params  接口参数，注意是mutable变量，可以直接修改
 */
- (void)filter:(NSString*)service params:(NSMutableDictionary*)params {
    // 在此对接口进行过滤
}
@end</code></pre>
<p>然后设置过滤器：</p>
<pre><code class="language-Objective-C">[[[[[[AFNPhalApiClient sharedClient] withHost:@"http://api1.aevit.xyz/"] withService:@"Default.Index"] withParams:params]
      withFilter:[MyFilter new]] // filter
     requestWithFormDataBlock:^(id formData) {
    } completeBlock:^(id resultObject) {
    } failureBlock:^(NSError *error) {
}];</code></pre>
<h3>(2)扩展结果解析器</h3>
<p>当返回的接口结果不是JSON格式时，如XML，请上google搜索“AFNetworking XML”相关资料即可。  </p>
<hr />
<h4><a href="/wikis/%5B6.3%5D-SDK%E5%8C%85%EF%BC%88PHP%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/#">下一章</a></h4>
<h1>[6.5]-SDK包（javascript版）</h1>
<p>todo#[6.6]-SDK包（Ruby版）</p>
<h4><a href="/wikis/%5B6.5%5D-SDK%E5%8C%85%EF%BC%88javascript%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.4%5D-SDK%E5%8C%85%EF%BC%88object-c%E7%89%88%EF%BC%89.html">下一章</a></h4>
<hr />
<h2>推荐使用Gem</h2>
<p>推荐使用：<a href="https://github.com/phalapi/phalapi-sdk-ruby">PhalApi SDK for Ruby</a> 。  </p>
<h2>6.6.1 使用说明</h2>
<p>将框架目录下的 ./SDK/Ruby/PhalApiClient 目录中的全部代码拷贝到项目里面即可使用。  </p>
<h2>6.6.2 代码示例</h2>
<p>如下是使用的代码场景片段。  </p>
<p>首先，我们需要导入SDK包：</p>
<pre><code class="language-javascript">#demo.rb

require_relative './PhalApiClient/phalapi_client'

a_client = PhalApi::Client.create.withHost('http://demo.phalapi.net')
a_response = a_client.withService('Default.Index').withParams('username', 'dogstar').withTimeout(3000).request()

puts a_response.ret, a_response.data, a_response.msg</code></pre>
<h2>6.6.3 运行效果</h2>
<p>运行后，可以看到：  </p>
<pre><code class="language-javascript">200
{"title"=&gt;"Hello World!", "content"=&gt;"dogstar您好，欢迎使用PhalApi！", "version"=&gt;"1.2.1", "time"=&gt;1445741092}
</code></pre>
<h2>6.5.4 更多调用</h2>
<p>当需要重复调用时，需要先进行 <strong>重置操作</strong> ，如：</p>
<pre><code class="language-javascript">#one more time
a_response = a_client.reset \
    .withService("User.GetBaseInfo") \
    .withParams("user_id", "1") \
    .request

puts a_response.ret, a_response.data, a_response.msg</code></pre>
<p>当请求有异常时，返回的 ret!= 200，如：</p>
<pre><code class="language-javascript">#illegal request
a_response = a_client.reset.withService('XXXX.noThisMethod').request

puts a_response.ret, a_response.data, a_response.msg</code></pre>
<p>以上的输出为： </p>
<pre><code class="language-javascript">--------------------
400
非法请求：接口服务XXXX.noThisMethod不存在
</code></pre>
<h2>6.6.5 扩展你的过滤器和结果解析器</h2>
<h3>(1)扩展过滤器</h3>
<p>当服务端接口需要接口签名验证，或者接口参数加密传送，或者压缩传送时，可以实现此过滤器，以便和服务端操持一致。  </p>
<p>当需要扩展时，分两步。首先，需要实现过滤器接口：  </p>
<pre><code class="language-javascript">class MyFilter &lt; PhalApi::ClientFilter 
        def filter(service, *params)
            #TODO ...
        end
}</code></pre>
<p>然后设置过滤器：</p>
<pre><code class="language-javascript">a_response = PhalApi::Client.create.withHost('http://demo.phalapi.net') \
       .withFilter(MyFilter.new) \
       # ... \
       .request</code></pre>
<h3>(2)扩展结果解析器</h3>
<p>当返回的接口结果不是JSON格式时，可以重新实现此接口。  </p>
<p>当需要扩展时，同样分两步。类似过滤器扩展，这里不再赘述。</p>
<h2>6.6.6 一如既往的单元测试</h2>
<p>最后，附一张单元测试运行的效果图：<br />
<img src="http://7xiz2f.com1.z0.glb.clouddn.com/QQ20151025123152.png" alt="" /> </p>
<hr />
<h4><a href="/wikis/%5B6.5%5D-SDK%E5%8C%85%EF%BC%88javascript%E7%89%88%EF%BC%89.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B6.4%5D-SDK%E5%8C%85%EF%BC%88object-c%E7%89%88%EF%BC%89.html">下一章</a>#[5.1]-架构与思想：PhalApi核心设计和思想解读</h4>
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.2%5D-%E6%9D%82%E8%B0%88%EF%BC%9A%E6%89%AF%E4%B8%80%E4%BA%9BPhalApi%E7%9A%84%E5%89%8D%E4%B8%96%E5%92%8C%E4%BB%8A%E7%94%9F.html">下一章</a></h4>
<hr />
<p><em>设计软件有两种方法：一种是简单到明显没有缺陷，另外一种复杂到缺陷不那么明显。 --托尼.霍尔</em>  </p>
<h2>5.1.1 前言</h2>
<p>在软件工程这一学科和行业里，关于软件工程的解说有很多。有人说开发是一门艺术；有人说开发是一种技艺；也有人说开发是一门哲学。但个人认同从实用主义和理性的角度去理解。  </p>
<p>例如一个框架，我们之所以认为它好是因为我们发现这个框架遵循了编程规范、适当地使用了设计模式、巧妙地结合了设计原则、有着稳定的依赖、代码复杂度低、并且有着很高代码覆盖率的单元测试等等。  </p>
<p>也就是说，好的框架都是可以被解释的。既然可以被解释量化，也就可以学习、参考和借鉴。  </p>
<h2>5.1.2 共性和可变性分析</h2>
<p>关于共性和可变性分析，在《设计模式解析》一书中有着非常到位的讲解。  </p>
<p>CVA是一种很容易的理念，按我的理解即： <strong>抽离共性、隔离变化</strong> 。有点类似易经里面的“变”与“不变”。  </p>
<p>诚然，在过去的教育中（包括大学在内的），对于软件开发都着重谈论面向对象开发，即OOD，以致于很多人都对面向对象开发产生了很大的误解。而这种误解所带来的实际情况就是： <strong>我们都在进行面向对象开发，但却是标准呆板的面向对象开发，缺少生气，缺少活力</strong> 。  </p>
<p>很多人，都没有把我们开发人员作为专业人士看待，甚至连我们自己都否认我们是专业的。所以很多时候当产品提出需求时，我们提供的开发周期往往会被外界以讲价的方式削减。何以？为什么医生给出的手术时间病人没有讨价呢？因为很简单，在病人的眼里，医生是专业的。  </p>
<p>若我们也想达到专业的层次时，何以为？学习、思考和实践，我认为至少这三者是必不可少的。  </p>
<p>所以，当我们在对PhalApi进行设计时，我们进行了一次又一次地酝酿、尝试、思考。我们在思考：这些功能是否真的会在实际项目中被使用？开发人员是否可能很好地进行扩展？此种决策是否便于单元测试、从思路上减少代码异味？。。。  </p>
<p>我们谨记敏捷开发，不过度设计。但我们也确实需要一种思想上的指导。正好，我们看到了 <strong>共性和可变性的分析（commonality and variability analysis, CVA）</strong> 。 </p>
<h2>5.1.3 CVA和三种视角、抽象类之间的关系</h2>
<p>引用一下《设计模式解析》一书中的图表：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_04c1b193d2cdbb012cb3db54bbe44649" alt="apic" />  </p>
<p>在这种理念的指导下，我们会更愿意将接口开发过程的共性抽离统一起来，而可变性部分的则可以由开发人员根据不同的项目情况进行快速定制实现。</p>
<h2>5.1.4 不稳定性与抽象度分布</h2>
<p>除了常谈及到的“低耦合、高内聚”外，在对代码进行静态分析和衡量其可维护度时，还有一个值得注意的值，即：不稳定的度量。  </p>
<p>不稳定的度量可以根据以下公式计算获得：</p>
<pre><code>I = 离心耦合 / (离心耦合 + 向心耦合)</code></pre>
<p>因此从宏观上，我们的代码结构，从上层到下层，应该向着稳定的方向递增，也就是说越底层越应稳定。对应 <strong><em>稳定依赖原则的规则（SDP），包之间的依赖应该朝着稳定的方向：不稳定的包应该依赖于更稳定的包。</em></strong>    </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_23af8d40e592df6f4a870873871d3a04" alt="1233112" />  </p>
<p>又结合不稳定性与抽象分布图，我们PhalApi框架的代码 <strong>应该大部分分布在上图中的抽象稳定区以实现框架高层的建设、少部分分布在具体不稳定区以提供一些基础的功能</strong> 。</p>
<h2>5.1.5 创建和使用分离</h2>
<p>在框架的特性中，包括：可重用、IoC Container以及SOLID五条原则的运用等。这里就部分SOLID原则的运用简单说明一下。  </p>
<h3>(1)S：单一职责原则</h3>
<p>这是我们一直都坚持遵守的原则，因为，我们也坚持 <strong>短而美</strong> 的写法， <strong>致力于编写优雅的代码、编写人容易理解的代码</strong> 。</p>
<h3>(2)O：开放-封闭原则</h3>
<p>我们首先希望的是在进行接口开发过程中，当需要新增一个接口时是开放的，对已有的响应调用流程是封闭的。即我们只需要实现新接口逻辑即可，不需要改动其他过程的调用。因此在OCP原则的指导下，我们通过结合工厂方法封装了对接口的初始化和调用。</p>
<h3>(3)D：依赖倒置原则</h3>
<p>PhalApi框架，最大的特色莫过于 <strong>它提供了一种如何快速进行接口开发的机制，但它不强制你使用不必要的功能，甚至它还鼓励你通过它来尝试研发自己的框架</strong> 。更进一步，PhalApi引入了新颖明确的概念，一如服务。我们把客户端调用的接口称之为接口服务，把服务端用到的资源称之为资源服务。对于后者，PhalApi提供了灵活的DI机制，以支持各项目定制化的开发。</p>
<h2>5.1.6 PhalApi核心架构图</h2>
<p><img src="http://webtools.qiniudn.com/20150411005257_3d974a8af4cde7bcafa4c4e227f71e97" alt="PhalApi-20150208" />  </p>
<p>显然，到目前为止，从核心架构图所折射出PhalApi的结构和代码是如此的 <strong>简单明了、统一规范</strong>。至少我是这么认为的，也是一直这样努力的。  </p>
<p>从上图的核心架构图可以看出，中间红色部分的DI处于汇点位置，提供各种资源服务的定位、创建、管理和提供。  </p>
<p>而左上角的代码示例则表达本系统框架运行的主流程： <strong>创建一个接口实例，运行响应</strong>。  </p>
<p>右上解黄色部分则为多变的接口应用开发的代码，这里特意罗列了两组接口，意在表明可以在此框架下挂靠多套接口。  </p>
<p>最下面是接口开发过程中所用到的各种基础设施和技术，如日记、配置读取、缓存、加密、请求和响应等。同样，除各应用项目中形式多变的接口开发外，这块的底层技术亦支撑不一而足的需求。因为，PhalApi只是作了共性的抽离，即提供一级抽象且稳定的接口或者抽象类，以约定规约视角中接口的函数签名，不作过多的具体实现。同时以DI作为辅助，支持快速扩展。</p>
<h2>5.1.7 PhalApi核心执行流程</h2>
<p>和其他框架不同，除了有文档对基本使用有说明外，我们还提供了我们框架核心的设计和思想，以便大家洞明其中的原理从而进一步优化扩展。  </p>
<p>这里，扼要说明一下PhalApi框架中接口请求背后的核心执行流程。  </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_54efc4d7bcb670c547a884cce43ad5ac" alt="PhalApi-接口处理主流程 - 0227" /></p>
<p>从上图的时序图中可以看出，在PhalApi中，一个接口的请求处理，只要分为两个环节： <strong>接口服务初始化</strong> 和 <strong>接口服务调用</strong> 。  </p>
<h3>(1)接口服务初始化</h3>
<p>在Web Service中，往往需要对服务进行注册发布后，才能开放请求。这里免去这一层，但遵循 <strong>创建和使用分离</strong> 的原则，我们将接口服务的初始化进行了封装，以便可以统一进行初始化、异常处理和一些权限ACL的控制</p>
<p>，甚至接口访问的统计等操作，更为重要的是接口开发人员可以进行无绪开发，而不需要过多知道如何合法创建接口服务。  </p>
<p>在1.2. 步骤中，UML时序图中的::generateService()表示对静态函数的调用，即对应代码：</p>
<pre><code class="language-javascript">PhalApi_ApiFactory::generateService();</code></pre>
<p>随后，可以看到（假设我们这次请求的服务为：?service=Demo.DoSth），我们创建了一个指定接口的实例（此接口类须继承于PhalApi_Api基类），并以变量a返回实例。  </p>
<p>如果请求的服务非法，则会以 <strong>400非法请求</strong> 返回给客户端。而正确创建接口服务a后，则会进行接口的初始化，其中有接口参数规则的解析和注册了过滤器服务后的检测操作。  </p>
<p>当这一系列的操作都成功执行后，我们将会得到一个接口服务实例a返回。<br />
因此，在接口服务的创建过程中，我们没有过多地限制，而是预留了很大的空间给到接口项目定制开发。</p>
<p>至此，接口服务的创建完成。</p>
<h3>(2)接口服务调用</h3>
<p>在完成复杂的创建工作后，客户端（备注：这里指的是服务端开发的开发客户端）只需要简单调用需要进行的操作即可。  </p>
<p>而这一块，则需要接口项目具体开发实现，也是我们项目级的核心部分。  </p>
<p>在获取接口服务的背后，我们建议结合领域驱动设计的理念，对项目代码进行这样的层级划分：  </p>
<ul>
<li>Api接口层：用于接收参数并响应接口的请求；</li>
<li>Domain领域层：用于处理复杂的领域业务逻辑，保证规则只出现一次；</li>
<li>Model数据源层：更广义上的Model层，提供数据来源，不限于DB；</li>
</ul>
<p>最后，是我们客户端关心的返回格式。  默认情况下，我们都是以JSON格式返回的，但仍然可以轻松支持其他格式的返回，如JSONP、XML等。只需要简单地开发实现，然后重新注册即可。  </p>
<p>至此，接口服务的调用完毕。</p>
<h2>5.1.8 DI支持下的轻松扩展</h2>
<p>当使用一个开源框架时，我们既喜欢其强大的一面，但矛盾的同时我们又害怕其中的复杂性，原因莫过于：学习成本高、出现问题时怕驾驭不了。  </p>
<p>而在这里，在PhalApi这里，这一切都是这么简单，简单地又如此明了。  </p>
<p>当需要进行资源服务的扩展时，我们可以：</p>
<h3>实现自己需要的服务</h3>
<p>实现指定资源服务在规约视角约定的接口，假设我们需要用文件来当作新的缓存存储。则需：</p>
<pre><code class="language-javascript">class MyCache_File implements PhalApi_Cache {
    public function set($key, $value, $expire = 600) {
        //...
    }

    public function get($key) {
        //...
    }

    public function delete($key) {
        //...
    }
}</code></pre>
<h3>在入口重新注册</h3>
<p>当实现自己的功能后，只需要简单地在入口文件重新注册即可。如：</p>
<pre><code class="language-javascript">DI()-&gt;cache = new MyCache_File();</code></pre>
<p>最后，另人兴奋的是，原来全部的调用代码都不需要改动，即可享受后期调整升级后的新功能！完全避免了曾经那种“牵一发而动全身”的痛苦。并且，定制开发出来的实现类，还可以跨越业务在其他项目中共用。  </p>
<p>这不正是我们常常所说的代码重用吗？而如今，我们很优雅地做到了！  </p>
<p>然而，我们在实际开发中收获到的远远不是代码重用这么简单，而是一种更好的开发实践。因为通过DI使得创建和使用分离，所以我们可以让高级的开发同学实现服务功能的开发，然后再提供给普通的开发同学使用，新手亦然，因为对他们来说：会用就行。当然，对于高级的同学，还应该遵循开发的最佳实践，坚持单元测试，以保证我们提供了可靠的接口（广义上的接口，非HTTP请求的接口）给我们的“客户端”使用。  </p>
<p>若如此，我们的开发合作岂不是很更合理、更明朗、更愉快？哈哈，我想是的。</p>
<p>作为一个框架，我们应当以发散的方式去设计；但为了能为应用提供可用的功能，我们又应当以收敛的方式去实现。<br />
如果我们提供的功能不足以满足大部分主流的业务场景，那么我们至少需要提供可扩展的空间。  </p>
<p>正是出于这样的考虑，我们虔诚地引入了DI。</p>
<hr />
<h4><a href="#">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.2%5D-%E6%9D%82%E8%B0%88%EF%BC%9A%E6%89%AF%E4%B8%80%E4%BA%9BPhalApi%E7%9A%84%E5%89%8D%E4%B8%96%E5%92%8C%E4%BB%8A%E7%94%9F.html">下一章</a>#[5.2]-杂谈：扯一些PhalApi的前世和今生</h4>
<h4><a href="/wikis/%5B5.1%5D-%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%80%9D%E6%83%B3%EF%BC%9APhalApi%E6%A0%B8%E5%BF%83%E8%AE%BE%E8%AE%A1%E5%92%8C%E6%80%9D%E6%83%B3%E8%A7%A3%E8%AF%BB.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.3%5D-%E6%A1%86%E6%9E%B6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%9C%AF%E8%AF%AD%E8%A1%A8%E5%92%8CPHP%E5%BC%80%E5%8F%91%E5%BB%BA%E8%AE%AE.html">下一章</a></h4>
<hr />
<p><em>此篇章与框架技术无关，为课外读物，感兴趣的可以略读一下。</em></p>
<h2>5.2.1 写在前面的话</h2>
<p>记得曾经有一位大主教在自己的墓志铭上写道：</p>
<pre><code>“小时候，我发誓要改变整个世界；
大些了，我努力改变我的祖国；
后来，我想我能改变我居住的这个城市；
直到临死之前才发现，我只能改变自己，从而影响他人，乃至影响整个世界”。</code></pre>
<p>当然，我没有想过要去影响整个世界，我只想尽我自己的所学，为自己，为他人做一些有用的事情。  </p>
<p>正如，最近在thephp.cc站点上面看到的这样一张PPT：</p>
<p><img src="http://webtools.qiniudn.com/20150411005257_30831e386e6cd1c171ba4486c1b5166f" alt="-1" /></p>
<p>我不一定能记住这位国际出名的前辈的名字，但我记住了他的这句话：  </p>
<pre><code>Driven by his passion to help developers build better software.</code></pre>
<h2>5.2.2 然而，要布道，先要践道。</h2>
<p>所以，当我希望把PhalApi框架开源出去时，我这个框架应该是初步完善并可以应用于实际项目开发的，至少我已经切身用过它来进行后台接口项目开发并不断优化重构之。  </p>
<h2>5.2.3 最初框架源于毕业论文的需要</h2>
<p>当年，在进行毕业论文的项目开发时，因为需要用到后台接口。于是乎，便到图书馆和网上查阅整理了很多资料，知识理论包括RESTFull、phprpc、SOAP协议、Aamzon接口服务、WSDL、Web Service等等。  </p>
<p>然而，在实际中我却发现很多人在用PHP开发接口是很简单的，或者说是很粗爆的，如直接使用fopen()函数进行获取接口结果。并且，当我需要去寻找一个可以进行后台接口开发的框架时，居然没找到很好框架支持。基于此，最初的接口框架就在这样的背景和研究下出来了。  </p>
<p>附一下当前毕业论文的相关资料：</p>
<ul>
<li>PhalApi前世： 毕业论文答辩汇报 - 精简版.ppt</li>
<li>PhalApiu前世：毕业论文 - 暗夜在火星 - 修改版.pdf</li>
<li>PhalApi前世文档：zenWS3完全开发手册 - 初稿 - 20130317.pdf</li>
</ul>
<p>可以在  <a href="http://git.oschina.net/dogstar/PhalApi/attach_files">附件</a>  下载上面的资料。  </p>
<h2>5.2.4 最初框架的首次商用和实站</h2>
<p>在毕业后就职的第一家公司中，由于也有接口开发的需要，正好是管理后台 内部系统调用，所以在征得领导同意后，我将我毕业论文所研发的接口框架进行了部署和开发。首次投入了商业用途。<code>^_^</code></p>
<p>并同时整理出了初步的接口框架，也就是PhalApi的前世。</p>
<h2>5.2.5 PhalApi的前世</h2>
<p>最初的框架命名是：zenphpWS3。<br />
原因是zen表示开源、php表示用PHP开发、WS表示Web Service、3表示支持SOAP/HTTP/RPC三种协议以及JSON/XML/ARRAY等多种格式的返回。<br />
更多可以访问我之前的博客： <a href="http://my.oschina.net/u/256338/blog/285184">zenphpWS3：文档&amp;示例&amp;设计&amp;源码</a>  </p>
<h2>5.2.6 又一年的全职接口开发和开源框架学习</h2>
<p>曾经有一年，我负责某个大型游戏项目的后台接口开发，同时提供接口供安卓版、iOS版和PC使用，对后台接口的开发有了更多的实战经验。  </p>
<p>并且，在对很多国外优秀的开源框架的学习和研究后，有了很多的领会，如我个人最为推荐的Phalcon。Phalcon不仅仅在框架方面做得非常优秀，在文档编写方面也是不遗余力的，即使我们不用这个框架，也能从这个框架的文档上吸引很多有用的思想。  </p>
<p>才发现最初提供的接口框架还有很多不成熟的地方。。。</p>
<h2>5.2.7 PhalApi框架开源的前奏</h2>
<p>到后来，在越来越多项目开发的需要下，出于仍然没有很好的接口框架支持（这里补充说明一下：一搜索“php接口开发框架”没有好的结果；二所接触到的框架虽然有支持接口开发，但只是其中一部分）。更为重要的是，项目往往又是如此重要，很担忧所选的框架进行接口开发后遇到问题不知如果在底层修正。  </p>
<p>所以，还是用回了自己的接口开发框架。  </p>
<p>当然，在做项目接口开发的同时，我也有意识地在对接口框架进行完善和重构，并融入框架所需要的特性、原则和模式。如可重用、IoC、5条设计原则、组件等。  </p>
<p>至此，PhalApi接口框架也就慢慢地明朗了。接下来就是：</p>
<h2>5.2.8 PhalApi正式走向开源</h2>
<p>如今，很多大的企业都提供了开放平台，如腾讯开放平台、新浪微博开放平台、优酷开放平台等。与此相比，虽然小应用轻量级App项目的后台接口没有这么大的规模，但也是很有量级上的需求的，正如下面这张图所展示的。 </p>
<p><img src="http://webtools.qiniudn.com/20150411005257_4df8d1c65122c2e46bf09bce0bb5b2b5" alt="222" /></p>
<p>在此大背景下，显然会有更多的应用需要后台接口的支撑，需要进行后台接口的开发，乃至对海量数据和访问的支持、快速扩容升级、一键发布、多端支持等。即使这样，PHP开发人员在接口开发时，仍然会显得有点“随心所欲”，缺少规范和统一，更为重要的是仍然没有发现好的PHP接口开发框架。 </p>
<pre><code>古云此日足可惜，吾辈更应惜秒阴。</code></pre>
<p>所以，此框架通过我多个实际项目应用后，在不断整理重构、精益迭代下走向了开源。<code>^_^</code></p>
<p>希望对你们有所帮助，谢谢。</p>
<p>我们通过这个框架，希望可以（正如文档里面所说的）：</p>
<pre><code>一来，支撑轻量级项目后台接口的快速开发；
二来，阐明如何进行接口开发、设计和维护，以很好支持海量访问、大数据、向前向后兼容等；
三来，顺便分享一些好的思想、技巧和有用的工具、最佳实践。</code></pre>
<p>其中，第一点是直接的也是最表面的，即你通过这个接口框架就可以快速开发了；第二点是更通用的接口开发中的一些好的或者必要的技术支撑和策略；第三点是面向框架级别的，即使你不用这个接口框架，也可以设计研发自己的开发框架，同时也是突破接口开发、超越PHP语言的。  </p>
<p>书不尽言，欢迎多交流，再次谢谢！</p>
<hr />
<h4><a href="/wikis/%5B5.1%5D-%E6%9E%B6%E6%9E%84%E4%B8%8E%E6%80%9D%E6%83%B3%EF%BC%9APhalApi%E6%A0%B8%E5%BF%83%E8%AE%BE%E8%AE%A1%E5%92%8C%E6%80%9D%E6%83%B3%E8%A7%A3%E8%AF%BB.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.3%5D-%E6%A1%86%E6%9E%B6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%9C%AF%E8%AF%AD%E8%A1%A8%E5%92%8CPHP%E5%BC%80%E5%8F%91%E5%BB%BA%E8%AE%AE.html">下一章</a>#[5.3]-框架总结：术语表和PHP开发建议</h4>
<h4><a href="/wikis/%5B5.2%5D-%E6%9D%82%E8%B0%88%EF%BC%9A%E6%89%AF%E4%B8%80%E4%BA%9BPhalApi%E7%9A%84%E5%89%8D%E4%B8%96%E5%92%8C%E4%BB%8A%E7%94%9F.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.4%5D-%E8%AE%B8%E5%8F%AF.html">下一章</a></h4>
<hr />
<h2>5.3.1 术语表</h2>
<p>我们并不想“制造”一些新的术语来增加大家的学习成本，但为了更高效地进行专业交流，我们将PhalApi框架中所用到的一些概念进行提炼并罗列如下。</p>
<h3>(1)接口服务</h3>
<p>通常，我们把远程第三方提供的接口称为API。但秉承Web Service的概念，我们更愿称接口为服务。为了同时保留这两者的意思，我们在这里统一将API称为 <strong>接口服务</strong> 。也就是我们对应的?service=XXX.XXX 。  </p>
<h3>(2)资源服务</h3>
<p>在使用DI依赖注入进行注册的组件，我们也倾向称之为服务，同时也是一些服务端上可用的资源，如数据库、缓存、加解密等。因此统称为 <strong>资源服务</strong> 。</p>
<h2>5.3.2 PHP开发建议</h2>
<p>在实际项目开发过程中，通过观察不同开发人员编写的PHP代码，会很趣。因为你会发现每个人的编程风格都不尽相同，但我们更提倡约定编程、规范的代码和编写人容易理解的代码。所以，下面就发现的问题进行说明。  </p>
<h3>(1)滥用的静态方法</h3>
<p>很多框架和很多项目都说自己在使用面向对象编程，但其实很多时候是在类中全部使用静态方法的伪面向对象。这里可能会引发争议，因为有些同学会认为静态类方法比成员函数更快速，而且也确实有相关的数据表明是快了一点点（严格上来讲，很微小）。但作为代价，我们失去的更多。如我们没能使用动态，也不方便在单元测试时使用桩、短件、模拟等技巧。更为重要的是，失去了高层的概念提炼和规约层的约定，不利于接口和实现地分离。  </p>
<p>所以，请只有在需要的时候才使用静态static方法，如工具类或实用操作。</p>
<h3>(2)非真正意义上的单元测试</h3>
<p>很多时候，我看到很多框架和项目中没有单元测试的代码，就算有也不是真正意义上的单元测试。可测试的代码是美的，因为可测试的代码表明职责单一明了，有低耦合度，可以进行快速模拟和替换。关于单元测试，前面已有文档详细说明。  </p>
<p>所以，请尽量尝试和坚持PHPUnit单元测试，体验测试驱动开发的乐趣，体验浮现式设计的激动。</p>
<h3>(3)无处不在的单例模式</h3>
<p>很多同学在学习了设计模式后，都很想试用一把，所以往往在很多时候是为了“用设计模式”而用设计模式，而不考虑是否合适，是否真的需要。尤其对于单例模式，这种情况更为普遍。  </p>
<p>使用单例模式的时机包括有：提高系统性能、全局只能有且只有一个实例否则会导致问题发生、提供一个全局的公共访问点等。  </p>
<p>然而其他很多情况则不需要。例如很多情况，实例为某容器所持有，则只需要在容器内做数量控制即可，不需要被持有的实例再作单例控制。  </p>
<p>所以，请只有确切需要使用单例时才使用。</p>
<h2>5.3.3 PhalApi框架的不足</h2>
<p>在我们不断维护、演进PhalApi框架的同时，我们也在使用这个框架进行了很多项目的开发，与此同时也在阅读各方面的书籍以获得更深层次的理解。<br />
在这样实践、思考、再设计的不断反馈迭代后，我们看到了PhalApi确实在某方面表现得出色。  </p>
<p>但一个负责任的框架，应该也明确指出它的不足。<br />
这里，我们将PhalApi开发中的不足罗列如下，希望为你进行框架设计或者对PhalApi的使用有更好的理解。  </p>
<h3>(1)接口结果中msg应该改名为error</h3>
<p>我们推荐的接口返回格式为：</p>
<pre><code class="language-javascript">{
    "ret": 200,
    "data": {
        "code": 0,   //对操作码进行说明
        ....         //更多结果的说明
        "msg": ""
    },
    "msg": ""
}</code></pre>
<p>显然，上面两个msg字段，会给开发团队带来困惑或混淆。<br />
更好是应该把最外层的msg改成error更为贴切，因为只有错误时此字段才有效。</p>
<p>但基于前期的大量文档说明，此外层的ret、data、msg三个字段已约定。所以，只能从应用层的msg进行重命名，如tips。</p>
<h3>(2)对NotORM中limit操作的错误优化</h3>
<p>前期，由于没有深刻留意MySql中OFFSET关键字的作用，导致了做了一些不精确的优化。  </p>
<p>可注意以下的微妙区别：</p>
<pre><code class="language-javascript">limit 5 OFFSET 10   #从第10个位置开始，查询前5个

limit 5, 10         #从第5个位置开始，查询前10个</code></pre>
<p>但重点考虑到如果修复这个之前犯下的错误，会对项目升级后有很大的冲动。<br />
可预料的故障有：”升级后，首页列表无任何数据显示“和“升级后，列表数据过多导致App加载崩溃”。</p>
<p>最后，出于对已在开发或已上线项目的保护和承诺向前兼容的原则，我们不得不保留了这个污点。<br />
所以，当对底层进行改动时，须确保已透彻理解各操作的微妙区别。</p>
<h3>(3)对数据库操作封装的欠缺</h3>
<p>一直以来，数据库支持这块都是比较欠缺的。所以我们使用了NotORM。  </p>
<p>但我们为了能更好把NotORM与PhalApi整合，将它调整成更适合我们的使用方式，我们又为NotORM提供了一个封装层，类似代理。  </p>
<p>然而，这会为新手入门这个框架造成一定的迷惑。<br />
因为，这有两套操作数据库的区别。但他们一开始不能很好地理解这样的区别，以及这样设计的初衷。  </p>
<p>坦白来说，PhalApi对于数据库这块不是好的设计，但好在它可以使用并正常地工作。</p>
<hr />
<h4><a href="/wikis/%5B5.2%5D-%E6%9D%82%E8%B0%88%EF%BC%9A%E6%89%AF%E4%B8%80%E4%BA%9BPhalApi%E7%9A%84%E5%89%8D%E4%B8%96%E5%92%8C%E4%BB%8A%E7%94%9F.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.4%5D-%E8%AE%B8%E5%8F%AF.html">下一章</a>  #[5.4]-许可</h4>
<h4><a href="/wikis/%5B5.3%5D-%E6%A1%86%E6%9E%B6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%9C%AF%E8%AF%AD%E8%A1%A8%E5%92%8CPHP%E5%BC%80%E5%8F%91%E5%BB%BA%E8%AE%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.5%5D-%E8%81%94%E7%B3%BB%E5%92%8C%E5%8A%A0%E5%85%A5%E6%88%91%E4%BB%AC.html">下一章</a></h4>
<hr />
<h2>5.4.1 协议许可的权利 - GPL协议</h2>
<p>PhalApi官方网站为 <a href="http://www.phalapi.net，依法独立拥有">http://www.phalapi.net，依法独立拥有</a> PhalApi 及官网发布的PhalApi衍生产品著作权。PhalApi及其衍生产品著作权受到法律和国际公约保护。使用者需仔细阅读本协议，在理解、同意、并遵守本协议的全部条款后，方可开始使用 PhalApi及其衍生 软件。  </p>
<p>本授权协议适用于 <a href="http://www.phalapi.net">http://www.phalapi.net</a> 发布的所有应用程序，PhalApi官方网站拥有对本授权协议的最终解释权。  </p>
<h2>5.4.2 协议规定的约束和限制</h2>
<p>PhalApi软件及其PhalApi官网发布的PhalApi衍生产品在没有相关说明的情况下默认是免费的，可用于商业用途或者非商业用途。但是无论用途如何，是否经过修改，修改程度如何，未经过许可， 页面上的产品名称以及指向官网的连接还有源代码中关于PhalApi的版权信息及作者信息必须保留，不许修改或删除，除非您获得PhalApi官网的许可。  </p>
<p>如果您未能遵守本协议的条款，您的授权将被终止，所被许可的权利将被收回，并承担相应法律责任。  </p>
<h2>5.4.3 有限担保和免责声明</h2>
<p>本软件及所附带的文件是作为不提供任何明确的或隐含的赔偿或担保的形式提供的。 用户出于自愿而使用本软件，您必须了解使用本软件的风险，在尚未购买产品技术服务之前，我们不承诺提供任何形式的技术支持、使用担保，也不承担任何因使用本软件而产生问题的相关责任。 PhalApi官网不对使用本软件及官网发布的衍生产品构建的软件中的文章或信息承担责任。  </p>
<p>有关 PhalApi 最终用户授权协议、商业授权与技术服务的详细内容，均由 PhalApi 官方网站独家提供。PhalApi官网拥有在不事先通知的情况下，修改授权协议和服务价目表的权力，修改后的协议或价目表对自改变之日起的新授权用户生效。 电子文本形式的授权协议如同双方书面签署的协议一样，具有完全的和等同的法律效力。您一旦开始安装 PhalApi以及官网发布的衍生产品，即被视为完全理解并接受本协议的各项条款，在享有上述条款授予的权力的同时，受到相关的约束和限制。 协议许可范围以外的行为，将直接违反本授权协议并构成侵权，我们有权随时终止授权，责令停止损害，并保留追究相关责任的权力。  </p>
<hr />
<h4><a href="/wikis/%5B5.3%5D-%E6%A1%86%E6%9E%B6%E6%80%BB%E7%BB%93%EF%BC%9A%E6%9C%AF%E8%AF%AD%E8%A1%A8%E5%92%8CPHP%E5%BC%80%E5%8F%91%E5%BB%BA%E8%AE%AE.html">上一章</a>   <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.5%5D-%E8%81%94%E7%B3%BB%E5%92%8C%E5%8A%A0%E5%85%A5%E6%88%91%E4%BB%AC.html">下一章</a>  #[5.5]-联系和加入我们</h4>
<h4><a href="/wikis/%5B5.4%5D-%E8%AE%B8%E5%8F%AF.html">上一章</a>    <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.6%5D-%E6%9B%B4%E6%96%B0%E6%97%A5%E8%AE%B0.html">下一章</a></h4>
<hr />
<h2>5.5.1 官网</h2>
<p><a href="http://www.phalapi.net">http://www.phalapi.net</a></p>
<h2>5.5.2 交流与咨询</h2>
<h3>(1)国际渠道</h3>
<p>邮箱：chanzonghuang@gmail.com</p>
<h3>(2)国内通道</h3>
<p>QQ：376741929<br />
QQ交流群：421032344  </p>
<h3>(3)博客与文档</h3>
<p>博客：<a href="http://my.oschina.net/dogstar/blog?catalog=3264711">http://my.oschina.net/dogstar/blog?catalog=3264711</a><br />
WIKI文档：<a href="http://git.oschina.net/dogstar/PhalApi/wikis">PhalApi Wiki</a>  </p>
<h3>(4)问题反馈</h3>
<p>Issues：<a href="http://git.oschina.net/dogstar/PhalApi/issues">http://git.oschina.net/dogstar/PhalApi/issues</a></p>
<h2>5.5.2 感谢以下贡献者</h2>
<pre><code>dogstar
火柴
George
zz.guo
文振熙
黄苗笋
爱编程的小逗比
Aevit
Summer
冰霜</code></pre>
<p>期待您的加入，您也可以通过其他的途径来支持我们，谢谢！</p>
<hr />
<h4><a href="/wikis/%5B5.4%5D-%E8%AE%B8%E5%8F%AF.html">上一章</a>    <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.6%5D-%E6%9B%B4%E6%96%B0%E6%97%A5%E8%AE%B0.html">下一章</a>  #[5.6]-更新日记</h4>
<h4><a href="/wikis/%5B5.5%5D-%E8%81%94%E7%B3%BB%E5%92%8C%E5%8A%A0%E5%85%A5%E6%88%91%E4%BB%AC.html">上一章</a>     <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.8%5D-%E8%87%B4%E6%A1%86%E6%9E%B6%E8%B4%A1%E7%8C%AE%E8%80%85%EF%BC%9A%E5%8A%A0%E5%85%A5PhalApi%E5%BC%80%E6%BA%90%E6%8C%87%E5%8D%97.html">下一章</a></h4>
<hr />
<p>PhalApi从1.2版本进入多人协助开发和项目使用后，也开始遵守严格的版本控制。我们平均每周会修复已发现的BUG，每2-4周发布一个小版本。  </p>
<h2>分支说明</h2>
<ul>
<li>release 发布分支，稳定的版本，可直接用于团队的项目开发</li>
<li>master 主线分支，随时待发布的版本，测试验收后会合并到release进行发布</li>
<li>master-X.X.X 主线的历史分支，以保留或查看历史版本</li>
<li>dev 开发分支，当前用于协同开发的分支，以存放共享未完成的代码</li>
</ul>
<h2>更新日记</h2>
<h3>2017-03-16</h3>
<pre><code>1、官网www.phalapi.net切换到https</code></pre>
<h3>2017-03-12</h3>
<pre><code>1、添加文件上传接口到七牛扩展，并补充完善其他扩展接口服务注释</code></pre>
<h3>2017-03-09</h3>
<pre><code>1、安装模块及语言模块更新，感谢@高一平 提供
2、微信支付接口修复，感谢@秋寒 提供</code></pre>
<h3>2017-03-02</h3>
<pre><code>1、listAllApis.php添加左侧菜单，便于多接口时文档分类显示，感谢 @初学者 提供
2、调试模式下，输出显示错误信息
3、添加英文文档：http://www.phalapi.net/wikis/en/，进度5%
4、把框架核心代码的注释翻译成英文，进度60%</code></pre>
<h3>2017-02-28</h3>
<pre><code>1、listAllApis.php支持显示扩展类库的接口 </code></pre>
<h3>2017-02-20</h3>
<pre><code>1、添加Python版本的SDK包，感谢@高一平 提供</code></pre>
<h3>2017-02-18</h3>
<pre><code>1、把原来master-x.x.x分支，重命名为archive-x.x.x
2、添加繁体翻译文件，感谢@高一平 提供</code></pre>
<h3>2017-02-13</h3>
<pre><code>1、补充部分缺失的翻译
2、开启英文版本的文档编写与代码版本注释
3、修复FastRoute扩展缺少POST/GET参数的BUG</code></pre>
<h3>2017-02-08</h3>
<pre><code>1、微信支付接口增加服务商支持 感谢@442484396/秋寒 提供</code></pre>
<h3>2017-02-05</h3>
<pre><code>1、phalapi-buildapp脚本中sed命令的修正MacOS support 与GNU SED兼容，感谢@x2x4 反馈提供
2、添加@exception异常注释支持，并补充文档：[1.32]-在线接口文档：注释规范
3、添加CLI扩展类库，用于开发命令行项目</code></pre>
<h3>2017-01-01 - v1.3.5幺鸡贺岁小版本</h3>
<pre><code>1、C# SDK包代码优化，改用Json库，感谢@X反馈提供
2、phalapi-buildapp及phalapi-buildcode两个命令脚本的格式转换及注释修正</code></pre>
<h3>2016-10-15</h3>
<pre><code>1、将PhalApi_Model_NotORM::getTableName($id)抽象方法改为返回默认表名的实现，以防实现此方法而引起的报错。
2、提供PhalApi SDK for Ruby，项目地址：https://github.com/phalapi/phalapi-sdk-ruby</code></pre>
<h3>2016-10-15</h3>
<pre><code>1、错误日记追加，按现有的日记格式补充到已有的日记载体，感谢@442484396反馈</code></pre>
<h3>2016-10-13</h3>
<pre><code>1、调试模式下，数据库连接失败时显示详细的错误信息，方便定位排查问题</code></pre>
<h3>2016-9-3</h3>
<pre><code>1、C# SDK包编码从Default调整为UTF8，避免接口接收数据为乱码，感谢@shero反馈</code></pre>
<h3>2016-8-7</h3>
<pre><code>1、优化logs机制,对脚本程序进行优化,感谢@石头上的露珠 反馈</code></pre>
<h3>2016-07-27 - v1.3.4 暑假炫酷版</h3>
<pre><code>1、在线接口文档样式美化，感谢@转身.满目疮痍提供
2、#53 断开数据库链接优化，进一步对数据库断开进行深度精除
3、#56 postgresql无法获取新增数据的主键ID优化，感谢@clov4r-连友提供解决方案
4、NotORM_Result::query()由protected提升为public级别，放开限制 感谢@scott反馈</code></pre>
<h3>2016-07-25</h3>
<pre><code>1、优化NotROM进行Count时的效率</code></pre>
<h3>2016-06-18</h3>
<pre><code>1、修复生成文档时对返回参数匹配中含有空格显示问题BUG,感谢@clov4r-连友提供</code></pre>
<h3>2016-05-21</h3>
<pre><code>1、对请求类进行更新,新增了getHeader方法获取头信息
2、修复notrom-insert_multi键名是关键字会出错BUG</code></pre>
<h3>2016-05-14</h3>
<pre><code>1、添加生成代码的脚本命令：phalapi-buildcode</code></pre>
<h3>2016-04-30</h3>
<pre><code>1、Task扩展添加MC的MQ支持</code></pre>
<h3>2016-04-22 - v1.3.3</h3>
<pre><code>1、优化三个命令脚本的使用说明，使其更友好、更专业
2、# 48 接口文档注释中有空格问题优化
3、官方视频教程系列录制与发布
4、V1.3.3小版本再次更新及发布公告</code></pre>
<h3>2016-04-6</h3>
<pre><code>1、维护install优化目录权限访问验证</code></pre>
<h3>2016-04-2</h3>
<pre><code>1、修复sting类型长度验证是中文字符占用三个字符问题,使用format下标可用于定义字符编码的类型，如utf8、gbk,gb2312
2、install优化目录权限访问验证</code></pre>
<h3>2016-03-26</h3>
<pre><code>1、V1.3.3小版本发布</code></pre>
<h3>2016-03-18</h3>
<pre><code>1、对Redis缓存进行扩展完善，感谢 @風 提供</code></pre>
<h3>2016-03-12</h3>
<pre><code>1、添加PhalApi客户端SDK包（Go版），感谢@prettyyjnic 提供
2、添加PhalApi客户端SDK包（React-Native版），感谢@ダSimVlove辉 提供
3、修复NotORM在调试时输出日记的warning</code></pre>
<h3>2016-02-13</h3>
<pre><code>1、PhalApi_Api第54行!isset($this-&gt;name)判断bugfixed</code></pre>
<h3>2016-01-18</h3>
<pre><code>1、框架中文名起用：π框架，感谢@猫哥 @Lambda! 两位的建议！
2、将JSON返回格式中的header调整为：application/json</code></pre>
<h3>2016-01-13</h3>
<pre><code>1、添加兼容iOS下mcrypt加密的扩展类库，感谢@大蝉 提供
2、默认Demo示例下，数据库表字段从from改名为note，以防误导
3、紧急修复数据库isKeepPrimaryKeyIndex写反的问题，并紧急发布</code></pre>
<h3>2016-01-02 - v1.3.2</h3>
<pre><code>1、修改notorm使用sql语句使用:和?替换执行sql时,对于int,bool,null进行单独处理
2、优化了PhalApi_Request_Formatter_File,支持了对文件上传类型验证增加了ext后缀名验证
3、提供CHM格式的wiki文档
4、初始化文件init.php简化，并默认装载扩展类库Library目录
5、修正install可视化安装中对listAllApis.php 和 checkApiParams.php 文件中的项目名称
6、其他一些轻量bugfixed</code></pre>
<h3>2015-12-31（非100%完美兼容）</h3>
<pre><code>1、listAllApis.php在线接口列表，追加对“更多说明”的使用支持，即使用注释@desc
2、添加PhalApi基础教程到官方wiki，感谢 @喵了个咪 提供
3、添加Object-C的SDK包XSHttpTool，感谢 @garden！ 提供
4、默认采用以数组的形式返回结果集，1.3.1及以下版本需要保持原来主键为下标的返回时，需要在init.php文件手动切换：DI()-&gt;notorm-&gt;keepPrimaryKeyIndex();</code></pre>
<h3>2015-11-27 - v1.3.1</h3>
<pre><code>1、重构并设计格式化类：PhalApi_Request_Var，使之支持项目扩展定制化；并添加callable回调类型、为数组和日期补充最值检测
2、PhalApi_DB_NotORM上添加对数据库维度的事务操作支持 感谢@研究研究 反馈
3、重构PhalApi_DB_NotORM，以便支持定制MySQL外的数据库操作
4、bugfixed NotORM_Result::fetchPairs()的错误: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given 感谢@scott 反馈
5、提供了Redis扩展类型，由 @喵了个咪 提供
6、添加支持Yaconf扩展读取配置
7、版本更新到v1.3.1 (感恩版)</code></pre>
<h3>2015-11-07</h3>
<pre><code>1、添加Ruby版SDK包
2、完善listAllApis.php，可按注释进行字典排列，支持多级目录扫描
3、添加了DB_Cluster扩展类库，由 @喵了个咪 提供
4、添加了Medoo扩展类库，由 @转身.满目疮痍 提供
5、DI()-&gt;notorm::queryAll()与DI()-&gt;notorm::queryRows()第二个参数$parmas改成缺省默认为array()</code></pre>
<h3>2015-10-25 - v1.3.0</h3>
<pre><code>1、用户扩展类库的完善，添加创建会话的入口，与文档完善
2、初始化文件init.php简化
3、提供可视化安装向导
4、首次线下活动尝试：官方非正式交流会第一期(广州站) --分享你在PHP接口开发中的心得与疑问
5、wiki文档二次系统整理
6、利用MDwiki部署全新的在线文档：http://wiki.phalapi.net/
7、官网首页版本迭代更新
8、添加示例目录结构图解 由@黄苗笋提供
9、listAllApis.php支持自动化生成 由@转身.满目疮痍 提供
10、落点应用框架：freeApi服务于互联网开发者的免费接口供应平台 由@转身.满目疮痍 提供，并正在致力开发完善中，请见：http://git.oschina.net/xiaoxunzhao/freeApi
11、提供基于接口查询语言的SDK包，并有JAVA版、PHP版、C#版、Objective-c版（由@Aevit 提供）、JS版（由@喵了个咪 提供）
12、PhalApi基础教程的编写 由@喵了个咪 提供，请见：http://my.oschina.net/wenzhenxi/blog/513004
13、添加了默认接口签名过滤器：PhalApi_Filter_SimpleMD5
14、加强对DI()-&gt;filter的类型检测，避免出现non-object的错误，增加容错能力
15、版本更新到v1.3.0 (1024狂欢版)</code></pre>
<h3>2015-09-07</h3>
<pre><code>1、添加扩展类库：基于FastRoute的快速路由
2、一些轻量bugfixed</code></pre>
<h3>2015-08-23</h3>
<pre><code>1、为接口参数规则扩展desc说明
2、补充了：[4.2] 开发实战2：模拟优酷开放平台接口项目开发
3、提供Auth 权限扩展 (由@黄苗笋提供，在这里表示再次感谢，^_^)
4、一些轻量bugfixed
5、版本更新到v1.2.1</code></pre>
<h3>2015-07-26</h3>
<pre><code>1、修正不能获取文件类型参数
2、修正在线接口参数查询工具中对默认值输出的展示
3、修正User扩展类库中登录态检测穿透，原因在于不该使用 isset(DI-&gt;cache) 进行判断</code></pre>
<h3>2015-07-11</h3>
<pre><code>1、计划任务Task扩展类库追加异常时的结果返回，以明确出错的原因
2、添加数据库配置注释</code></pre>
<h3>2015-06-27</h3>
<pre><code>1、自动生成SQL语句的脚本，追加对配置数据库字符集的读取，并无表时读取默认的路由配置以便可以生成SQL
2、User扩展类库的Bugfixed
3、docs文档更新</code></pre>
<h3>2015-06-06</h3>
<pre><code>1、接口返回参数，根据代码return注释，自动生成在线文档
2、接口参数规则中追加对文件参数的验证支持
3、将Task扩展类库转移并内置于PhalApi框架中
4、统一使用GPL协议
5、重构在线接口参数查看，使用OO的规范并提取成类放置在辅助类包中，以便接口更好地使用
6、提供统一的测试辅助类：PhalApi_Helper_TestRunner; 若需保留原来的写法，可追加： class PhalApiTestRunner extends PhalApi_Helper_TestRunner{}
7、一些bugfixed
</code></pre>
<h3>2015-05-23</h3>
<pre><code>1、添加redis缓存
2、提供计划任务扩展类库及对应的文档
3、提供项目实战示例和对应的文档
4、补充更多的文档
5、自动生成SQL语句的脚本改从./Data/目录下获取对应的建表文件，原来为./Config/sql/
6、提供一个接口开发实战示例（投票活动）
7、补充数据库的port和charset配置，port默认为3306，charset默认为UTF8
8、完成计划任务续篇，提供统一的crontab调度</code></pre>
<h3>2015-05-07</h3>
<pre><code>1、PhalApi_Cache_Memcached更正类名为PhalApi_Cache_Memcached，注意此类名的修正所带来的影响，可以在init.php文件检测项目是否有使用MC
2、追加更低版本的Memcahce支持
3、开启1.2版本，进入多人协助开发时代</code></pre>
<h3>2015-05-01</h3>
<pre><code>1、五一小更新，提供一致性的DB操作接口
2、添加swoole扩展类库，以支持长链接和异步任务
3、版本更新到1.1.4</code></pre>
<h3>2015-04-24</h3>
<pre><code>1、修正数据库连接失败时显露帐号信息</code></pre>
<h3>2015-04-17</h3>
<pre><code>1、官网完善，添加备案信息、下载、关于、许可等页面
2、对框架添加COOKIE支持、SL快速函数、CURL重试次数、字符串正则匹配验证等
3、开源用户扩展类库
4、创建PhalApi框架交流群</code></pre>
<h3>2015-04-11</h3>
<pre><code>1、添加在线接口列表模板，及生成脚本同步更新
2、文档移至官网维护
3、官网页面开发</code></pre>
<h3>2015-04-04</h3>
<pre><code>1、WIKI文档排版，与更多开篇的编写
2、源代码注释的对齐与再整理
3、生成项目脚本与创建项目的一些工作准备
4、提供单元测试Runner及phpunit.xml配置文件</code></pre>
<h3>2015-03-27</h3>
<pre><code>1、代码注释完善与在线类参考手册生成：http://www.phalapi.net/docs/</code></pre>
<h3>2015-03-21</h3>
<pre><code>1、一些bugfixed：规则下标开头大小写问题、文件缓存个数问题预防等
2、扩展类库：七牛云存储接口调用
3、文档整理和编写新的文档</code></pre>
<h3>2015-03-15</h3>
<pre><code>1、增加对RSA加密的支持，以及超长字符串的解决方案
2、文件缓存目录拆分，以支持海量的文件缓存
3、官网再上线：www.phalapi.net</code></pre>
<h3>2015-02-26</h3>
<pre><code>1、对之前的模型代码、查询类、多级缓存等补充完善单元测试
2、WIKI文档补充UML图示
3、Release 1.1.2 发布</code></pre>
<h3>2015-02-24</h3>
<pre><code>1、文件缓存、空缓存及多级缓存的引入
2、表数据入口的Model基类，以供NotORM系列子类使用，关键点为主键映射和分表情况
3、结合多级缓存和广义Model，对高成本的数据获取的应对方案
4、WIKI文档补充</code></pre>
<h3>2015-02-15</h3>
<pre><code>1、年前的更新：三篇文档的编写，到此基础入门只差多级缓存未完成；</code></pre>
<h3>2015-02-13</h3>
<pre><code>1、代码小酌，重构代码、整理注释，让代码更明了，更统一，更达意；
2、添加工具类，其中有：IP地址获取、随机字符串生成；
3、添加扩展类库：微信开发，并编写相关使用文档；
4、添加扩展类库：phprpc协议支持及WIKI编写；</code></pre>
<h3>2015-02-11</h3>
<pre><code>1、官网发布上线：http://112.74.107.125/，域名已申请，待绑定：www.phalapi.net；</code></pre>
<h3>2015-02-09</h3>
<pre><code>1、将原来限制于JSON格式的返回调整成更灵活的组件形式，以便支持JSON、JSONP和测试环境下的格式返回，和扩展项目开发所需要的格式；
2、接口基类的初始化函数PhalApi_Api::initialize()精简名字为init()；
3、全部核心的代码注释中的author追加个人邮箱地址；
4、单元测试完善补充和文档整理；</code></pre>
<h3>2015-02-07</h3>
<pre><code>1、完善接口调试下SQL的输出、示例和单元测试，以及WIKI文档的编写；
2、日记接口文档的编写；
3、合并master到release，并将版本号更新到1.1.1；</code></pre>
<h3>2015-02-04</h3>
<pre><code>1、根据质量分析后Sonar提供的报告，整理代码，包括统一的注释、对齐、代码风格、命名规则等；
2、默认服务的注册，有：DI()-&gt;request、DI()-&gt;response；</code></pre>
<h3>2015-02-02    版本1.1.0    一个全新的开始！</h3>
<pre><code>此版本在原来的基础上进行了大量的重构和更多的规范统一，主要有：
1、分离框架代码和项目代码，便于以后框架升级；
2、统一全部的入口文件，以便不同的版本、终端、入口和测试环境使用，并隐藏PHP语言实现；
3、框架代码统一从原来的Core_改名为PhalApi_，并且把PhalApi_DI::one()统一为快速函数的写法：DI()；
4、重新界定应用项目的代码目录结构，以包的形式快速开发；
5、全部文档相应更新；
//注意！此版本不兼容旧的写法，如有问题，请与我联系。</code></pre>
<h3>2015-02-01</h3>
<pre><code>1、正常时，ret返回调整为：200，原来为0；
2、异常统一简化为两大类：客户端非法请求400、服务端运行错误500；
3、日记文件写入重构，并将权限更改为777，以便不同场合下日记写入时的permission denied；
4、单元测试整理；</code></pre>
<h3>2015-01-31</h3>
<pre><code>1、参数规则的解析，移除不必要的固定类型，以及addslashes转换，单元测试整理；
2、参数规则文档编写：http://my.oschina.net/u/256338/blog/372947</code></pre>
<h3>2015-01-29</h3>
<pre><code>1、examples代码重新整理，及入门文档同步更新；
2、入口文件的调整；</code></pre>
<h3>2015-01-28</h3>
<pre><code>1、补充入门开发示例的文档，及相关的测试代码和产品代码，主要是examples；
2、提供接口参数在线查询工具；</code></pre>
<h3>2015-01-24</h3>
<pre><code>1、PhalApi开源；</code></pre>
<hr />
<h4><a href="/wikis/%5B5.5%5D-%E8%81%94%E7%B3%BB%E5%92%8C%E5%8A%A0%E5%85%A5%E6%88%91%E4%BB%AC.html">上一章</a>     <a href="/wikis/">文档首页</a>   <a href="/wikis/%5B5.8%5D-%E8%87%B4%E6%A1%86%E6%9E%B6%E8%B4%A1%E7%8C%AE%E8%80%85%EF%BC%9A%E5%8A%A0%E5%85%A5PhalApi%E5%BC%80%E6%BA%90%E6%8C%87%E5%8D%97.html">下一章</a></h4>
            </div>
        </div>

        <div class="grid">
            <div class="grid__cell">
                <!-- TODO -->
            </div>
        </div>
    </div>

 <!-- 广告位 -->
<div class="grid-wrapper desktop-only">
	<p align="center">
		<a href="http://7xiz2f.com1.z0.glb.clouddn.com/%E6%88%91%E7%9A%84%E5%90%8D%E5%AD%97%E5%8F%AB%EF%BC%9A%CF%80%E6%A1%86%E6%9E%B6%20-%20PhalApi%202016%E5%B9%B4%E5%BA%A6%E5%BC%80%E6%BA%90%E6%80%BB%E7%BB%93%20-%20%E5%AE%98%E6%96%B9%E5%87%BA%E5%93%81.pdf" target="blank"><img width="950" height="100" src="http://7xiz2f.com1.z0.glb.clouddn.com/ad_20170104.png"></a>
		<a href="http://www.itran.cc/" target="blank"><img width="950" height="100" src="http://7xslqv.com1.z0.glb.clouddn.com/images/9/69/b88bc92455dc1239f9a5bf8d72929.png"></a>
		<a href="http://web-tools.phalapi.net/" target="blank"><img width="950" height="100" src="http://7xslqv.com1.z0.glb.clouddn.com/images/7/d0/88f523566c482296aecc43d185ca2.png"></a>
	</p>
</div>

<!-- footer(S) -->
<div class="grid-wrapper footer">
	<div id="footer" class="grid">	

		<div class="grid__cell">
			<h3>PhalApi (π框架)</h3>
            <p>
				<font size="3px">PHP开源框架，专注于接口开发，支持HTTP/SOAP/RPC协议，可用于快速搭建微服务、RESTful接口或Web Services。</font>
				
				<br />

				<font size="2px">
				友情链接： <a href="https://www.phalapi.net/" class="menu-item__link" target="_blank"><strong>PhalApi</strong></a>
				| <a href="http://www.oschina.net/" class="menu-item__link" target="_blank">开源中国</a>
				| <a href="https://www.itran.cc/" class="menu-item__link" target="_blank">艾翻译</a>
				| <a href="http://www.phalconphp.com/en/" class="menu-item__link" target="_blank">Phalcon</a>
				| <a href="https://phpunit.de/manual/3.7/zh_cn/automating-tests.html" class="menu-item__link" target="_blank">PHPUnit</a>
				| <a href="http://www.thoughtworks.com/cn/" class="menu-item__link" target="_blank">ThoughtWorks</a>

				<br />

				&copy;2015-2017 PhalApi All Rights Reserved. <a href="http://www.miitbeian.gov.cn" target="_blank" >粤ICP备15028808号</a>
				</font>

				<script type="text/javascript">var cnzz_protocol = (("https:" == document.location.protocol) ? " https://" : " http://");document.write(unescape("%3Cspan id='cnzz_stat_icon_1255326144'%3E%3C/span%3E%3Cscript src='" + cnzz_protocol + "s4.cnzz.com/z_stat.php%3Fid%3D1255326144%26show%3Dpic' type='text/javascript'%3E%3C/script%3E"));</script>
			</p>
		</div>	
	</div>
</div>
<!-- footer(E) -->

</body>

</html>